# Claude Code Setup for Pictory API Source: https://docs.pictory.ai/ai-tools/claude-code Configure Claude Code to call the Pictory API from your terminal — set up auth, project context, and proven prompts [Claude Code](https://claude.com/claude-code) is Anthropic's official CLI for Claude. This page shows you how to configure it to work productively with the Pictory API: making API calls, building integration code, debugging payloads, and operating as a Pictory-aware coding assistant. ## Prerequisites * Claude Code installed (`npm install -g @anthropic-ai/claude-code`) * A Claude subscription (Pro, Max, or API access) * A Pictory API key from [app.pictory.ai/api-access](https://app.pictory.ai/api-access) (starts with `pictai_`) ## Setup ```bash theme={null} npm install -g @anthropic-ai/claude-code ``` Add the key to your shell profile (do not commit it): ```bash theme={null} echo 'export PICTORY_API_KEY="pictai_your_key_here"' >> ~/.zshrc source ~/.zshrc ``` ```bash theme={null} cd /path/to/your/project claude ``` Create a `CLAUDE.md` at your project root using the template in the next section. This teaches Claude Code the Pictory API conventions before you ask any questions. ## Recommended `CLAUDE.md` Template Drop this into your project root. Claude Code will load it automatically every session. ```markdown theme={null} # Pictory API project rules ## About this project This project integrates with the Pictory API to create videos programmatically. ## API conventions - **Base URL:** `https://api.pictory.ai/pictoryapis` - **Authentication:** `Authorization: $PICTORY_API_KEY` header. **The key value is raw — do NOT use a Bearer prefix.** - **API key format:** Always starts with `pictai_`. Get it from https://app.pictory.ai/api-access. - **Content-Type:** All POST requests use `application/json`. ## Primary endpoints | Endpoint | Purpose | |---|---| | `POST /v2/video/storyboard` | Create a storyboard preview | | `POST /v2/video/storyboard/render` | Render final video directly | | `POST /v2/projects/{projectid}/render` | Re-render an existing project | | `GET /v1/jobs/{jobid}` | Fetch job status and output | | `GET /v1/brands/video` | List video brand kits | | `GET /v1/avatars` | List AI avatars | ## Field rules - `videoName` is required on all render endpoints. - `brandId` and `brandName` are **mutually exclusive** — never include both. - `smartLayoutId` and `smartLayoutName` are mutually exclusive. - `subtitleStyleId` and `subtitleStyleName` are mutually exclusive. - API-rendered jobs do NOT appear in My Projects unless `saveProject: true` is set, OR an existing `projectId` is passed as `templateId`. - Supported `language` values: `zh, nl, en, fr, de, hi, it, ja, ko, mr, pt, ru, es, ta`. ## Job lifecycle 1. Submit a render → receive `jobId`. 2. Either poll `GET /v1/jobs/{jobid}` every 10–30 seconds, OR pass a `webhook` URL in the request body. 3. When `status === "completed"`, the video URL is in `data.videoURL`. ## Reference documentation - Full docs: https://docs.pictory.ai/llms-full.txt - OpenAPI spec: https://docs.pictory.ai/openapi.json - API reference: https://docs.pictory.ai/api-reference ## Coding conventions for this project - Always read the `PICTORY_API_KEY` from `process.env` (Node) or `os.environ` (Python). Never inline. - Always handle the polling loop with exponential backoff or a fixed 10–30s interval — never tighter. - Always check `data.status` before reading `data.videoURL` — the field is absent until completion. ``` ## Example Prompts After Claude Code loads `CLAUDE.md`, you can prompt it naturally: * "Write a Python script that takes a recipe (list of steps as strings) and submits each step as a scene to the storyboard render endpoint. Use my chef avatar (`chef_07`)." * "Debug this payload — I am getting a 400. Here is the JSON: \[paste]" * "Add webhook support to my existing `submit_render.js` so I do not have to poll." * "Write a Make.com HTTP module configuration that calls the Pictory render API with a template ID and dynamic recipe scenes from the previous module." ## Connecting via MCP If you want Claude Code to call the Pictory API as a structured tool (rather than generating cURL/code), connect the Pictory MCP server. Setup details at [Pictory MCP Server](https://pictory.ai/pictory-mcp-server-api). ## Troubleshooting **Cause:** Training-data bias toward OAuth/Bearer flows. **Resolution:** Reinforce the rule in `CLAUDE.md`: "Authorization header value is the raw key with no prefix." If it still happens mid-session, correct it once — Claude Code will pick up the correction. **Cause:** Insufficient context loaded. **Resolution:** Add this line to your prompt: "Verify the endpoint exists by referencing [https://docs.pictory.ai/openapi.json](https://docs.pictory.ai/openapi.json) before suggesting it." Or fetch `llms-full.txt` and paste relevant sections. **Cause:** Code is missing `saveProject: true` or `templateId`. **Resolution:** Update `CLAUDE.md` to require one of these on all render calls. ## Next Steps System prompts, MCP, and example flows Ready-to-run JSON payloads Same setup, for Cursor users Same setup, for Windsurf users # Cursor Setup for Pictory API Source: https://docs.pictory.ai/ai-tools/cursor Configure Cursor to call the Pictory API with project rules, MCP integration, and proven prompts [Cursor](https://cursor.com) is an AI-first code editor. This page shows you how to configure it for productive work against the Pictory API: project-level rules that teach Cursor the API conventions, MCP integration for direct API calls, and example prompts. ## Prerequisites * Cursor installed * A Pictory API key from [app.pictory.ai/api-access](https://app.pictory.ai/api-access) (starts with `pictai_`) ## Setup Open the project where you want to integrate the Pictory API. Add the key to your shell profile (do not commit it): ```bash theme={null} echo 'export PICTORY_API_KEY="pictai_your_key_here"' >> ~/.zshrc source ~/.zshrc ``` Cursor inherits environment variables from the shell you launched it from. On Windows, use a `.env` file at the project root and add `.env` to `.gitignore`. Cursor reads `.cursor/rules/*.mdc` files automatically. Create the directory and rules file below. ```bash theme={null} mkdir -p .cursor/rules ``` Use the template in the next section to populate `.cursor/rules/pictory-api.mdc`. ## Recommended Cursor Rules Create `.cursor/rules/pictory-api.mdc`: ```markdown theme={null} --- description: "Pictory API conventions and rules" alwaysApply: true --- # Pictory API project rules ## About this project This project integrates with the Pictory API to create videos programmatically. ## API conventions - **Base URL:** `https://api.pictory.ai/pictoryapis` - **Authentication:** `Authorization: $PICTORY_API_KEY` header. **The key value is raw — do NOT use a Bearer prefix.** - **API key format:** Always starts with `pictai_`. Get it from https://app.pictory.ai/api-access. - **Content-Type:** All POST requests use `application/json`. ## Primary endpoints | Endpoint | Purpose | |---|---| | `POST /v2/video/storyboard` | Create a storyboard preview | | `POST /v2/video/storyboard/render` | Render final video directly | | `POST /v2/projects/{projectid}/render` | Re-render an existing project | | `GET /v1/jobs/{jobid}` | Fetch job status and output | | `GET /v1/brands/video` | List video brand kits | | `GET /v1/avatars` | List AI avatars | ## Field rules - `videoName` is required on all render endpoints. - `brandId` and `brandName` are **mutually exclusive** — never include both. - `smartLayoutId` and `smartLayoutName` are mutually exclusive. - `subtitleStyleId` and `subtitleStyleName` are mutually exclusive. - API-rendered jobs do NOT appear in My Projects unless `saveProject: true` is set, OR an existing `projectId` is passed as `templateId`. - Supported `language` values: `zh, nl, en, fr, de, hi, it, ja, ko, mr, pt, ru, es, ta`. ## Job lifecycle 1. Submit a render → receive `jobId`. 2. Either poll `GET /v1/jobs/{jobid}` every 10–30 seconds, OR pass a `webhook` URL in the request body. 3. When `status === "completed"`, the video URL is in `data.videoURL`. ## Reference documentation - Full docs: https://docs.pictory.ai/llms-full.txt - OpenAPI spec: https://docs.pictory.ai/openapi.json ## Coding conventions for this project - Read `PICTORY_API_KEY` from environment variables — never inline. - Polling intervals must be 10–30 seconds. - Always check `data.status` before reading `data.videoURL`. ``` ## OpenAPI as a Tool Schema Cursor's Agent mode can ingest an OpenAPI spec as a structured tool. Add this to your rules: ```markdown theme={null} When the user asks for API integration code, always reference the OpenAPI spec at https://docs.pictory.ai/openapi.json for accurate field names, types, and required parameters. ``` ## Connecting via MCP To let Cursor's Agent mode call the Pictory API as a tool (instead of generating code), connect the Pictory MCP server. Setup at [Pictory MCP Server](https://pictory.ai/pictory-mcp-server-api). ## Example Prompts * "Generate a TypeScript client for the Pictory storyboard render endpoint, using the OpenAPI spec at [https://docs.pictory.ai/openapi.json](https://docs.pictory.ai/openapi.json)." * "Build a React component that lets a user paste a script, choose a language and voice, and submit it to the Pictory render API." * "Add error handling for 400 responses to this Pictory integration — surface the validation message to the user." * "Refactor my polling loop to use 15-second intervals and exponential backoff capped at 60 seconds." ## Troubleshooting **Cause:** Default LLM bias toward Bearer flows. **Resolution:** Reinforce the rule in `.cursor/rules/pictory-api.mdc`. If Cursor still suggests Bearer in a session, correct it once and the agent will adapt. **Cause:** Common causes: missing `videoName`, both `brandId` and `brandName` included, or invalid `language` code. **Resolution:** Paste the API error response back into Cursor — it will adjust. Also confirm your rules file lists the field constraints. **Cause:** Cursor was launched before the env var was exported in your shell. **Resolution:** Restart Cursor after sourcing your `.zshrc` / `.bashrc`. On Windows, restart Cursor after setting the env var via System Properties. ## Next Steps System prompts, MCP, and example flows Ready-to-run JSON payloads Same setup, for Claude Code users Same setup, for Windsurf users # Windsurf Setup for Pictory API Source: https://docs.pictory.ai/ai-tools/windsurf Configure Windsurf's Cascade to call the Pictory API with workspace rules, MCP integration, and proven prompts [Windsurf](https://windsurf.com) is an agentic IDE powered by Cascade. This page shows you how to configure it for productive work against the Pictory API: workspace rules that teach Cascade the API conventions, MCP integration for direct API calls, and example prompts. ## Prerequisites * Windsurf installed * A Pictory API key from [app.pictory.ai/api-access](https://app.pictory.ai/api-access) (starts with `pictai_`) ## Setup Open the project where you want to integrate the Pictory API. Add the key to your shell profile (do not commit it): ```bash theme={null} echo 'export PICTORY_API_KEY="pictai_your_key_here"' >> ~/.zshrc source ~/.zshrc ``` Restart Windsurf after setting the variable. Windsurf reads `.windsurf/rules.md` from the project root. Create the directory: ```bash theme={null} mkdir -p .windsurf ``` Use the template in the next section to populate `.windsurf/rules.md`. ## Recommended Windsurf Rules Create `.windsurf/rules.md`: ```markdown theme={null} # Pictory API workspace rules ## About this project This project integrates with the Pictory API to create videos programmatically. Cascade should reference the rules below on every interaction touching API calls. ## API conventions - **Base URL:** `https://api.pictory.ai/pictoryapis` - **Authentication:** `Authorization: $PICTORY_API_KEY` header. **The key value is raw — do NOT use a Bearer prefix.** - **API key format:** Always starts with `pictai_`. Get it from https://app.pictory.ai/api-access. - **Content-Type:** All POST requests use `application/json`. ## Primary endpoints | Endpoint | Purpose | |---|---| | `POST /v2/video/storyboard` | Create a storyboard preview | | `POST /v2/video/storyboard/render` | Render final video directly | | `POST /v2/projects/{projectid}/render` | Re-render an existing project | | `GET /v1/jobs/{jobid}` | Fetch job status and output | | `GET /v1/brands/video` | List video brand kits | | `GET /v1/avatars` | List AI avatars | ## Field rules - `videoName` is required on all render endpoints. - `brandId` and `brandName` are **mutually exclusive** — never include both. - `smartLayoutId` and `smartLayoutName` are mutually exclusive. - `subtitleStyleId` and `subtitleStyleName` are mutually exclusive. - API-rendered jobs do NOT appear in My Projects unless `saveProject: true` is set, OR an existing `projectId` is passed as `templateId`. - Supported `language` values: `zh, nl, en, fr, de, hi, it, ja, ko, mr, pt, ru, es, ta`. ## Job lifecycle 1. Submit a render → receive `jobId`. 2. Either poll `GET /v1/jobs/{jobid}` every 10–30 seconds, OR pass a `webhook` URL in the request body. 3. When `status === "completed"`, the video URL is in `data.videoURL`. ## Reference documentation - Full docs: https://docs.pictory.ai/llms-full.txt - OpenAPI spec: https://docs.pictory.ai/openapi.json ## Coding conventions - Read `PICTORY_API_KEY` from environment variables — never inline. - Polling intervals must be 10–30 seconds. - Always check `data.status` before reading `data.videoURL`. ``` ## Connecting via MCP Windsurf's Cascade supports MCP servers natively. To let Cascade call the Pictory API as structured tools, connect the Pictory MCP server. 1. Open Windsurf settings → **Cascade** → **Model Context Protocol**. 2. Add the Pictory MCP server configuration (details at [Pictory MCP Server](https://pictory.ai/pictory-mcp-server-api)). 3. Reload Cascade to pick up the new tools. Once connected, Cascade can invoke Pictory endpoints directly without generating code first. ## Example Prompts * "Build a Node.js worker that consumes a queue of video render requests and submits each one to Pictory. Use the OpenAPI spec at [https://docs.pictory.ai/openapi.json](https://docs.pictory.ai/openapi.json) for the request shape." * "Add a unit test for my `submitPictoryRender` function that mocks the API response." * "Trace through this 401 error — why is my Pictory API request being rejected?" * "Generate a webhook handler that receives Pictory's completion callback and uploads the rendered video to S3." ## Troubleshooting **Cause:** Default LLM bias toward Bearer flows. **Resolution:** Reinforce the rule in `.windsurf/rules.md`. If Cascade still suggests Bearer mid-session, correct it once and Cascade will adapt. **Cause:** MCP server not running or misconfigured. **Resolution:** Verify the MCP server URL and credentials, restart Windsurf, and check the Cascade panel for tool registration errors. **Cause:** Default LLM tendency to write tight polling loops. **Resolution:** The rules file specifies 10–30 second polling. If Cascade still generates a 1-second loop, paste the snippet back and say "follow the polling interval rule in `.windsurf/rules.md`." ## Next Steps System prompts, MCP, and example flows Ready-to-run JSON payloads Same setup, for Claude Code users Same setup, for Cursor users # Introduction Source: https://docs.pictory.ai/api-reference Welcome to the Pictory API documentation. Learn how to integrate AI-powered video creation into your applications. ## Welcome to Pictory API The Pictory API enables developers to integrate AI-powered video creation and editing capabilities directly into their applications. Build automated video workflows, create personalized video content at scale, and leverage Pictory's advanced features programmatically. Access your API key from the Pictory dashboard *** ## What You Can Build The Pictory API provides comprehensive access to create and manage video content: Create storyboard preview or render storyboard video Build reusable templates for consistent video production Generate transcripts and AI-powered video summaries Apply custom fonts, styles, and brand configurations Search and manage media assets for your videos Add professional audio tracks and background music Connect with AWS S3 and Vimeo for storage and hosting Track asynchronous processing tasks and results *** ## Quick Start Get started with the Pictory API in three simple steps: Sign up for a Pictory account and obtain your API key from the [API Access page](https://app.pictory.ai/api-access). Your API key will start with `pictai_` and must be kept secure. Test your API key by fetching your projects: ```bash theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v2/projects' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' ``` Create a text-to-video storyboard preview: ```bash theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v2/video/storyboard' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "videoName": "my_first_video", "scenes": [ { "story": "Welcome to Pictory! This is your first AI-generated video. Turn any text into engaging video content in seconds.", "createSceneOnEndOfSentence": true } ] }' ``` This returns a `jobId` that you can use to check the status and get the preview output. Poll the job status to get your video preview: ```bash theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/{jobId}' \ --header 'Authorization: YOUR_API_KEY' ``` When `status` is `completed`, you'll receive the preview output with scene thumbnails and renderParams. Browse the API reference documentation to discover all available endpoints and capabilities. *** ## Base URL All API endpoints use the following base URL: ``` https://api.pictory.ai/pictoryapis ``` **Critical: Use the Correct API Version** Different endpoints use different API versions (`/v1/` or `/v2/`). Always verify you are using the correct version for each endpoint. *** ## Authentication All API requests must be authenticated using your API key in the `Authorization` header. ### API Key Format ``` Authorization: pictai_your_api_key_here ``` **Keep Your API Key Secure**: Never expose your API key in client-side code, public repositories, or unsecured locations. Use environment variables and secure secret management. *** ## Common Workflows Explore these popular workflows to get started quickly: Turn text into engaging videos with natural-sounding AI narration Transform blog articles into video content with AI voice narration Convert podcast episodes and audio files into visual content Create videos with AI-generated images and visuals *** If you encounter rate limiting (HTTP 429), implement exponential backoff retry logic in your application. *** ## Best Practices ### Security 1. **Secure API Keys**: Store keys in environment variables or secret managers 2. **Rotate Keys**: Periodically rotate API keys for enhanced security ### Performance 1. **Cache Static Data**: Cache fonts, styles, and brands locally 2. **Batch Operations**: Group related API calls when possible *** ## Stay Connected Join our community and stay updated with the latest features, tips, and announcements: Get real-time updates, tips, and product announcements Professional insights, company news, and industry trends Visual inspiration, video tips, and creative content Video tutorials, feature demos, and how-to guides Join 20,000+ creators sharing tips and success stories Join our Reddit community for discussions and updates Chat with developers, get quick help, and share feedback # Get AI Credits Usage Source: https://docs.pictory.ai/api-reference/account/get-aicredits-usage GET https://api.pictory.ai/pictoryapis/v1/aicredits/usage Retrieve a transaction-level history of AI credit consumption and additions for your subscription ## Overview Retrieve a per-transaction ledger of AI credit activity on your subscription. Each entry represents a single event that affected your AI credit balance, such as credits consumed by an AI avatar generation or credits added by purchasing an add-on. By default, this endpoint returns transactions for the **current month**. You can query a specific month using the optional query parameters described below. Transactions are returned in **descending order by date** (newest first). Use this endpoint to power a credit usage dashboard, generate billing reports, or audit consumption against expected workloads. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. Usage history is available for the **last 12 months only**. Older transactions are not retained. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/aicredits/usage ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters The year to query (for example, `2026`). When omitted, the current year is used. Note: `year` cannot be passed without `month`. Year-only queries are not supported. The month to query, between `1` and `12`. When omitted, the current month is used. When `month` is provided without `year`, the current year is assumed. #### Query Behavior | Request | Returns | | ------------------------------------------- | ----------------------------------------------------------------------- | | `GET /aicredits/usage` | All transactions for the **current month** | | `GET /aicredits/usage?month=2` | February of the current year | | `GET /aicredits/usage?year=2026&month=3` | March 2026 | | `GET /aicredits/usage?year=2026` (no month) | **400** `INVALID_REQUEST` — `month` is required when `year` is provided | *** ## Response Returns an array of transaction objects. Each transaction describes a single AI credit event with its direction (`credit` or `debit`), amount, and source. Array of transaction objects, ordered by transaction date in descending order (newest first). The response body is the array itself. Unique identifier for this transaction. Direction of the transaction. One of: * `debit`: AI credits were consumed (for example, an AI avatar generation) * `credit`: AI credits were added to your balance (for example, an add-on purchase) Always-positive number of AI credits affected by this transaction. The direction is conveyed by `type`. May be a decimal (for example, `0.6` for a single Text-to-Image generation). Human-readable label describing the cause of the transaction. Example values include: * `AI Avatar` — credits consumed by an AI avatar generation * `Text-to-Image` — credits consumed by a text-to-image generation * `AI Video` — credits consumed by an AI video generation * `Courtesy Credits` — complimentary credits added to your account * `Add-on Purchase` — credits added by purchasing a credits add-on * `Promotional Credits` — credits added through a promotional campaign * `Monthly Reset` — credits reset at the start of a billing cycle * `Plan Change` — credits adjusted due to a subscription plan change * `Refund` — credits refunded after a failed operation Plain-language description of the transaction source. May be `null` for unknown sources. Subscription plan in effect at the time of the transaction. Example values: * `Professional Annual` * `API Annual` * `API Monthly` * `Teams Annual` * `Free Trial` Unrecognized plans fall back to the raw internal plan code. ISO 8601 timestamp of when the transaction occurred (UTC). ### Response Examples ```json 200 - Success theme={null} [ { "transactionId": "20260508153648354912f9ebd4ef94b1baa469ae1179a3ead", "type": "debit", "amount": 0.6, "source": "Text-to-Image", "description": "Credits consumed by a text-to-image generation", "plan": "API Annual", "transactionDate": "2026-05-08T15:37:11.176Z" }, { "transactionId": "20260501101259445721a0a11c9594db9b2577c844bc7338e", "type": "credit", "amount": 100, "source": "Monthly Reset", "description": "Credits reset at the start of the billing cycle", "plan": "API Annual", "transactionDate": "2026-05-01T10:12:59.445Z" }, { "transactionId": "20260305072219568c7b29f4c37ea4c41a49e89b5dba408f2", "type": "debit", "amount": 30, "source": "AI Avatar", "description": "Credits consumed by an AI avatar generation", "plan": "Professional Annual", "transactionDate": "2026-03-05T07:33:32.217Z" } ] ``` ```json 200 - Empty Results theme={null} [] ``` ```json 400 - Invalid Query Parameter theme={null} { "code": "INVALID_REQUEST", "message": "Query param 'year' must be a valid integer." } ``` ```json 400 - Invalid Month theme={null} { "code": "BAD_REQUEST", "message": "Invalid month. Month must be between 1 and 12." } ``` ```json 400 - Out of Window theme={null} { "code": "BAD_REQUEST", "message": "Usage history is only available for the last 12 months." } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "code": "BAD_CONFIGURATION", "message": "An unexpected error occurred while processing the request" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Current month curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/aicredits/usage' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool # Specific month curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/aicredits/usage?year=2026&month=3' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/aicredits/usage" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } params = {"year": 2026, "month": 3} response = requests.get(url, headers=headers, params=params) transactions = response.json() for t in transactions: direction = "+" if t["type"] == "credit" else "-" print(f"{t['transactionDate'][:10]} {direction}{t['amount']:>6} {t['source']}") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( "https://api.pictory.ai/pictoryapis/v1/aicredits/usage?year=2026&month=3", { method: "GET", headers: { Authorization: "YOUR_API_KEY", accept: "application/json", }, } ); const transactions = await response.json(); transactions.forEach((t) => { const direction = t.type === "credit" ? "+" : "-"; console.log(`${t.transactionDate.slice(0, 10)} ${direction}${t.amount} ${t.source}`); }); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" ) type Transaction struct { TransactionID string `json:"transactionId"` Type string `json:"type"` Amount float64 `json:"amount"` Source string `json:"source"` Description string `json:"description"` Plan string `json:"plan"` TransactionDate string `json:"transactionDate"` } func main() { req, _ := http.NewRequest("GET", "https://api.pictory.ai/pictoryapis/v1/aicredits/usage?year=2026&month=3", nil) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var transactions []Transaction json.Unmarshal(body, &transactions) for _, t := range transactions { direction := "-" if t.Type == "credit" { direction = "+" } fmt.Printf("%s %s%.0f %s\n", t.TransactionDate[:10], direction, t.Amount, t.Source) } } ``` # Get Current Quota Source: https://docs.pictory.ai/api-reference/account/get-quota GET https://api.pictory.ai/pictoryapis/v1/quota Retrieve the current usage and limits for the four core quota types on your subscription ## Overview Retrieve a real-time snapshot of the quotas associated with your Pictory subscription. The response shows how much of each quota you have used so far in the current billing term and how much is allowed by your plan. The endpoint reports the following four metrics: * **Video minutes** rendered through Pictory's video generation pipeline * **Transcription minutes** consumed by transcription jobs * **ElevenLabs VoiceOver minutes** used for AI text-to-speech * **AI credits** consumed by generative AI features such as AI avatars, text-to-image, and AI video generation Use this endpoint when you need to display a usage summary in your application, gate a feature based on remaining quota, or verify that a subscription has capacity before launching a long-running job. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/quota ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` This endpoint does not accept any path, query, or body parameters. The subscription is identified from the API key. *** ## Response Returns an object containing four usage entries. Every entry has the same shape: `used` reports the amount consumed in the current billing term and `limit` reports the maximum allowed by the plan. Video minutes rendered in the current billing term. Number of video minutes already consumed in the current billing term. Maximum number of video minutes allowed by your plan in one billing term. Transcription minutes consumed in the current billing term. Number of transcription minutes already consumed. Maximum number of transcription minutes allowed by your plan. ElevenLabs AI voice-over minutes consumed in the current billing term. Number of ElevenLabs voice-over minutes already consumed. Maximum number of ElevenLabs voice-over minutes allowed by your plan. AI credits consumed in the current billing term. AI credits power generative features such as AI avatars, text-to-image, and AI video generation. Number of AI credits already consumed. Total AI credit allowance for your subscription, including any add-on or promotional credits. ### Response Examples ```json 200 - Success theme={null} { "videoMinutes": { "used": 98, "limit": 14400 }, "transcriptionMinutes": { "used": 0, "limit": 14400 }, "elevenLabsVoiceOverMinutes": { "used": 33, "limit": 480 }, "aiCredits": { "used": 690, "limit": 22000 } } ``` ```json 400 - Bad Request theme={null} { "code": "UNAUTHORIZED", "message": "Missing subscription identity." } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```text 404 - Subscription Not Found theme={null} (empty response body) ``` ```json 500 - Internal Server Error theme={null} { "code": "BAD_CONFIGURATION", "message": "An unexpected error occurred while processing the request" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/quota' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/quota" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) quota = response.json() print(f"Video Minutes: {quota['videoMinutes']['used']}/{quota['videoMinutes']['limit']}") print(f"Transcription: {quota['transcriptionMinutes']['used']}/{quota['transcriptionMinutes']['limit']}") print(f"ElevenLabs VO: {quota['elevenLabsVoiceOverMinutes']['used']}/{quota['elevenLabsVoiceOverMinutes']['limit']}") print(f"AI Credits: {quota['aiCredits']['used']}/{quota['aiCredits']['limit']}") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( "https://api.pictory.ai/pictoryapis/v1/quota", { method: "GET", headers: { Authorization: "YOUR_API_KEY", accept: "application/json", }, } ); const quota = await response.json(); console.log(`Video Minutes: ${quota.videoMinutes.used}/${quota.videoMinutes.limit}`); console.log(`Transcription: ${quota.transcriptionMinutes.used}/${quota.transcriptionMinutes.limit}`); console.log(`ElevenLabs VO: ${quota.elevenLabsVoiceOverMinutes.used}/${quota.elevenLabsVoiceOverMinutes.limit}`); console.log(`AI Credits: ${quota.aiCredits.used}/${quota.aiCredits.limit}`); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" ) type QuotaEntry struct { Used int `json:"used"` Limit int `json:"limit"` } type Quota struct { VideoMinutes QuotaEntry `json:"videoMinutes"` TranscriptionMinutes QuotaEntry `json:"transcriptionMinutes"` ElevenLabsVoiceOverMinutes QuotaEntry `json:"elevenLabsVoiceOverMinutes"` AICredits QuotaEntry `json:"aiCredits"` } func main() { req, _ := http.NewRequest("GET", "https://api.pictory.ai/pictoryapis/v1/quota", nil) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var quota Quota json.Unmarshal(body, "a) fmt.Printf("Video Minutes: %d/%d\n", quota.VideoMinutes.Used, quota.VideoMinutes.Limit) fmt.Printf("Transcription: %d/%d\n", quota.TranscriptionMinutes.Used, quota.TranscriptionMinutes.Limit) fmt.Printf("ElevenLabs VO: %d/%d\n", quota.ElevenLabsVoiceOverMinutes.Used, quota.ElevenLabsVoiceOverMinutes.Limit) fmt.Printf("AI Credits: %d/%d\n", quota.AICredits.Used, quota.AICredits.Limit) } ``` # Generate Image Source: https://docs.pictory.ai/api-reference/ai-studio/generate-image POST https://api.pictory.ai/pictoryapis/v1/aistudio/images Generate an AI image from a text prompt using a selection of AI image models ## Overview The Generate Image API creates an AI-generated image based on a text prompt. You can choose from multiple AI image models, specify an aspect ratio, apply a visual style, and optionally provide a reference image to guide the output. The API returns a job ID that you can use to poll for the result once the image generation is complete. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/aistudio/images ``` *** ## Request Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body A text description of the image you want to generate. The prompt must be between 5 and 5,000 characters. **Example:** `"A serene mountain landscape at sunset with a reflective lake in the foreground"` The AI model to use for image generation. Each model produces different visual styles and quality levels. Defaults to `seedream3.0` if not specified. **Supported models:** `seedream3.0`, `flux-schnell`, `nanobanana`, `nanobanana-pro` **Model Capabilities and Pricing:** | Model | Supported Aspect Ratios | AI Credits per Image | | ---------------- | ----------------------- | -------------------- | | `seedream3.0` | `1:1`, `16:9`, `9:16` | 2 | | `flux-schnell` | `1:1`, `16:9`, `9:16` | 0.6 | | `nanobanana` | `1:1`, `16:9`, `9:16` | 4 | | `nanobanana-pro` | `1:1`, `16:9`, `9:16` | 14 | The aspect ratio for the generated image. The available values depend on the selected model. Defaults to the first supported aspect ratio of the chosen model. Refer to the model table above for supported aspect ratios per model. An optional visual style to apply to the generated image. **Supported values:** `photorealistic`, `artistic`, `cartoon`, `minimalist`, `vintage`, `futuristic` An optional URL of a reference image to guide the generation. The URL must be a valid, publicly accessible URI. An optional webhook URL that will receive a notification when the image generation job completes. The URL must be a valid URI. *** ## Response ### Success Response (200) `true` when the job has been created successfully The unique identifier (UUID) of the image generation job. Use this ID to poll for the result using the [Get Image Generation Job](/api-reference/ai-studio/get-image-job) endpoint. ### Response Examples ```json 200 - Success theme={null} { "success": true, "data": { "jobId": "4d82d040-4394-4371-867d-72ae1dd62b6d" } } ``` ```json 400 - Validation Error theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "field": "prompt", "message": "\"prompt\" is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "message": "Internal Server Error" } ``` *** ## Status Codes | Status Code | Description | | ----------- | ------------------------------------------------------------------------------ | | **200** | Job created successfully. Use the returned `jobId` to poll for the result. | | **400** | Invalid request body. Check the `fields` array for specific validation errors. | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key from the [API Access page](https://app.pictory.ai/api-access). ```bash cURL theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v1/aistudio/images' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "prompt": "A serene mountain landscape at sunset with a reflective lake in the foreground", "model": "seedream3.0", "aspectRatio": "16:9", "style": "photorealistic" }' ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/aistudio/images" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "prompt": "A serene mountain landscape at sunset with a reflective lake in the foreground", "model": "seedream3.0", "aspectRatio": "16:9", "style": "photorealistic" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): job_id = data["data"]["jobId"] print(f"Image generation job created: {job_id}") else: print(f"Error: {data}") ``` ```javascript JavaScript theme={null} const url = 'https://api.pictory.ai/pictoryapis/v1/aistudio/images'; const response = await fetch(url, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A serene mountain landscape at sunset with a reflective lake in the foreground', model: 'seedream3.0', aspectRatio: '16:9', style: 'photorealistic' }) }); const data = await response.json(); if (data.success) { console.log(`Image generation job created: ${data.data.jobId}`); } else { console.log('Error:', data); } ``` ```php PHP theme={null} "A serene mountain landscape at sunset with a reflective lake in the foreground", "model" => "seedream3.0", "aspectRatio" => "16:9", "style" => "photorealistic" ]); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); if ($data['success']) { echo "Image generation job created: " . $data['data']['jobId'] . "\n"; } else { echo "Error: " . json_encode($data) . "\n"; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v1/aistudio/images" payload := map[string]string{ "prompt": "A serene mountain landscape at sunset with a reflective lake in the foreground", "model": "seedream3.0", "aspectRatio": "16:9", "style": "photorealistic", } jsonPayload, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload)) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var data map[string]interface{} json.Unmarshal(body, &data) if success, ok := data["success"].(bool); ok && success { jobData := data["data"].(map[string]interface{}) fmt.Printf("Image generation job created: %s\n", jobData["jobId"]) } else { fmt.Println("Error:", string(body)) } } ``` *** ## Next Steps After receiving the `jobId`, poll for the image generation result using the [Get Image Generation Job](/api-reference/ai-studio/get-image-job) endpoint. Use a polling interval of **10–30 seconds** to check the job status. # Generate Video Source: https://docs.pictory.ai/api-reference/ai-studio/generate-video POST https://api.pictory.ai/pictoryapis/v1/aistudio/videos Generate an AI video from a text prompt using a selection of AI video models ## Overview The Generate Video API creates an AI-generated video based on a text prompt. You can choose from multiple AI video models, specify an aspect ratio and duration, and optionally provide a first frame image, a video to extend, or reference images to guide the output. The API returns a job ID that you can use to poll for the result once the video generation is complete. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/aistudio/videos ``` *** ## Request Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body A text description of the video you want to generate. The prompt must be between 5 and 5,000 characters. **Example:** `"A drone flying over a lush green forest with sunlight filtering through the canopy"` The AI model to use for video generation. Each model offers different quality levels, durations, and aspect ratios. Defaults to `pixverse5.5` if not specified. **Supported models:** `veo3.1`, `veo3.1_fast`, `pixverse5.5` **Model Capabilities and Pricing:** | Model | Supported Aspect Ratios | Supported Durations | AI Credits per Second | | ------------- | ----------------------------------- | ------------------- | --------------------- | | `veo3.1` | `16:9`, `9:16` | `4s`, `6s`, `8s` | 20 | | `veo3.1_fast` | `16:9`, `9:16` | `4s`, `6s`, `8s` | 10 | | `pixverse5.5` | `16:9`, `9:16`, `1:1`, `3:4`, `4:3` | `5s`, `8s`, `10s` | 1.6 | The aspect ratio for the generated video. The available values depend on the selected model. Defaults to the first supported aspect ratio of the chosen model. Refer to the model table above for supported aspect ratios per model. The duration of the generated video. The available values depend on the selected model. Defaults to the first supported duration of the chosen model. Refer to the model table above for supported durations per model. An optional URL of an image to use as the first frame of the video. This guides the visual starting point for the generation. The URL must be a valid, publicly accessible URI. This parameter cannot be used together with `extendVideoUrl` or `referenceImageUrls`. An optional URL of an existing video to extend. The AI model will generate additional content that continues from the end of the provided video. The URL must be a valid, publicly accessible URI. This parameter cannot be used together with `firstFrameImageUrl`. An optional array of image URLs (1 to 3) to use as visual references for the generated video. The AI model will use these images to influence the style and content of the output. Each URL must be a valid, publicly accessible URI. This parameter cannot be used together with `firstFrameImageUrl` or `extendVideoUrl`. An optional webhook URL that will receive a notification when the video generation job completes. The URL must be a valid URI. *** ## Response ### Success Response (200) `true` when the job has been created successfully The unique identifier (UUID) of the video generation job. Use this ID to poll for the result using the [Get Video Generation Job](/api-reference/ai-studio/get-video-job) endpoint. ### Response Examples ```json 200 - Success theme={null} { "success": true, "data": { "jobId": "4a09edf9-d071-4847-bda5-8dea913d80fe" } } ``` ```json 400 - Validation Error theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "field": "prompt", "message": "\"prompt\" is required" } ] } ``` ```json 400 - Invalid Parameter Combination theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "field": "extendVideoUrl", "message": "\"extendVideoUrl\" is not allowed" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "message": "Internal Server Error" } ``` *** ## Status Codes | Status Code | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Job created successfully. Use the returned `jobId` to poll for the result. | | **400** | Invalid request body. Check the `fields` array for specific validation errors. This also occurs when mutually exclusive parameters are used together. | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Parameter Exclusivity The `firstFrameImageUrl`, `extendVideoUrl`, and `referenceImageUrls` parameters are mutually exclusive. You may use only one of these in a single request. Providing more than one will result in a `400` validation error. | Parameter | Can be combined with | | -------------------- | ------------------------------------------------------------ | | `firstFrameImageUrl` | `prompt`, `model`, `aspectRatio`, `duration`, `webhook` only | | `extendVideoUrl` | `prompt`, `model`, `aspectRatio`, `duration`, `webhook` only | | `referenceImageUrls` | `prompt`, `model`, `aspectRatio`, `duration`, `webhook` only | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key from the [API Access page](https://app.pictory.ai/api-access). ```bash cURL theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v1/aistudio/videos' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "prompt": "A drone flying over a lush green forest with sunlight filtering through the canopy", "model": "pixverse5.5", "aspectRatio": "16:9", "duration": "8s" }' ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/aistudio/videos" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "prompt": "A drone flying over a lush green forest with sunlight filtering through the canopy", "model": "pixverse5.5", "aspectRatio": "16:9", "duration": "8s" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): job_id = data["data"]["jobId"] print(f"Video generation job created: {job_id}") else: print(f"Error: {data}") ``` ```javascript JavaScript theme={null} const url = 'https://api.pictory.ai/pictoryapis/v1/aistudio/videos'; const response = await fetch(url, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: 'A drone flying over a lush green forest with sunlight filtering through the canopy', model: 'pixverse5.5', aspectRatio: '16:9', duration: '8s' }) }); const data = await response.json(); if (data.success) { console.log(`Video generation job created: ${data.data.jobId}`); } else { console.log('Error:', data); } ``` ```php PHP theme={null} "A drone flying over a lush green forest with sunlight filtering through the canopy", "model" => "pixverse5.5", "aspectRatio" => "16:9", "duration" => "8s" ]); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); if ($data['success']) { echo "Video generation job created: " . $data['data']['jobId'] . "\n"; } else { echo "Error: " . json_encode($data) . "\n"; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v1/aistudio/videos" payload := map[string]string{ "prompt": "A drone flying over a lush green forest with sunlight filtering through the canopy", "model": "pixverse5.5", "aspectRatio": "16:9", "duration": "8s", } jsonPayload, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload)) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var data map[string]interface{} json.Unmarshal(body, &data) if success, ok := data["success"].(bool); ok && success { jobData := data["data"].(map[string]interface{}) fmt.Printf("Video generation job created: %s\n", jobData["jobId"]) } else { fmt.Println("Error:", string(body)) } } ``` *** ## Next Steps After receiving the `jobId`, poll for the video generation result using the [Get Video Generation Job](/api-reference/ai-studio/get-video-job) endpoint. Use a polling interval of **10–30 seconds** to check the job status. # Get Image Generation Job Source: https://docs.pictory.ai/api-reference/ai-studio/get-image-job GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} Retrieve the status and output of an AI image generation job ## Overview This endpoint retrieves the current status and output of an AI image generation job using its unique job ID. While the generation is in progress, it returns the job status. Once the image generation completes, it returns the output including the image URL, dimensions, and AI credits consumed. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the image generation job. This is the `jobId` returned by the [Generate Image](/api-reference/ai-studio/generate-image) endpoint. **Example:** `"4d82d040-4394-4371-867d-72ae1dd62b6d"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response ### In-Progress Response Returned while the image is being generated: The unique identifier of the image generation job `true` while the job is processing `"in-progress"` while the image is still being generated ### Completed Response Returned when the image has been generated successfully: The unique identifier of the image generation job `true` when the job has completed successfully Contains the image generation output. `"completed"` when the image has been generated successfully Direct download URL for the generated image file (PNG) Width of the generated image in pixels Height of the generated image in pixels Number of AI credits consumed for this image generation ### Failed Response Returned when the image generation job has failed: The unique identifier of the image generation job `false` when the job has failed `"failed"` when the image generation has failed Error code identifying the failure type (e.g., `"5001"`) Descriptive message explaining the cause of the failure ### Response Examples ```json 200 - In Progress theme={null} { "job_id": "4d82d040-4394-4371-867d-72ae1dd62b6d", "success": true, "data": { "status": "in-progress" } } ``` ```json 200 - Completed theme={null} { "job_id": "4d82d040-4394-4371-867d-72ae1dd62b6d", "success": true, "data": { "width": 1280, "aiCreditsUsed": 2, "height": 720, "url": "https://example.cloudfront.net/images/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/f8e7d6c5-b4a3-2190-fedc-ba0987654321.png", "status": "completed" } } ``` ```json 200 - Failed theme={null} { "job_id": "4d82d040-4394-4371-867d-72ae1dd62b6d", "success": false, "data": { "status": "failed", "error_code": "5001", "error_message": "Image generation failed due to an internal error." } } ``` ```json 200 - Invalid Job ID theme={null} { "id": "4d82d040-4394-4371-867d-72ae1dd62b6d", "success": false, "data": { "error_code": "5000", "error_message": "JOB_NOT_FOUND" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Status Codes | Status Code | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Request processed successfully. Check the `status` field in `data` to determine the job state (`in-progress`, `completed`, or `failed`). | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **404** | Job not found. The provided `jobid` does not match any existing job. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Completed Response Fields | Field | Description | | --------------- | ----------------------------------------------------- | | `url` | Direct download link for the generated PNG image file | | `width` | Width of the generated image in pixels | | `height` | Height of the generated image in pixels | | `aiCreditsUsed` | Number of AI credits consumed for this generation | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and use the `jobId` returned from the [Generate Image](/api-reference/ai-studio/generate-image) endpoint. ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/4d82d040-4394-4371-867d-72ae1dd62b6d' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests import time def poll_image_job(api_key, job_id, max_wait=300, poll_interval=15): """ Poll for image generation job completion. Args: api_key: Your Pictory API key job_id: The image generation job ID max_wait: Maximum wait time in seconds (default 5 minutes) poll_interval: Polling interval in seconds (default 15 seconds) """ url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}" headers = { "Authorization": api_key, "accept": "application/json" } start_time = time.time() while time.time() - start_time < max_wait: response = requests.get(url, headers=headers) data = response.json() if not data.get("success"): error_data = data.get("data", {}) print(f"Job failed: {error_data.get('error_message', 'Unknown error')}") return data status = data.get("data", {}).get("status") if status == "in-progress": print("Image generation in progress...") time.sleep(poll_interval) continue if status == "completed": result = data["data"] print(f"Image generated successfully!") print(f"Image URL: {result.get('url')}") print(f"Dimensions: {result.get('width')}x{result.get('height')}") print(f"AI Credits Used: {result.get('aiCreditsUsed')}") return data if status == "failed": error_msg = data["data"].get("error_message", "Unknown error") print(f"Image generation failed: {error_msg}") return data print(f"Unexpected status: {status}") return data print("Timeout waiting for image generation") return None # Usage result = poll_image_job("YOUR_API_KEY", "4d82d040-4394-4371-867d-72ae1dd62b6d") ``` ```javascript JavaScript theme={null} async function pollImageJob(apiKey, jobId, maxWait = 300000, pollInterval = 15000) { const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`; const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); if (!data.success) { console.log(`Job failed: ${data.data?.error_message || 'Unknown error'}`); return data; } const status = data.data?.status; if (status === 'in-progress') { console.log('Image generation in progress...'); await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } if (status === 'completed') { console.log('Image generated successfully!'); console.log(`Image URL: ${data.data.url}`); console.log(`Dimensions: ${data.data.width}x${data.data.height}`); console.log(`AI Credits Used: ${data.data.aiCreditsUsed}`); return data; } if (status === 'failed') { console.log(`Image generation failed: ${data.data?.error_message}`); return data; } console.log(`Unexpected status: ${status}`); return data; } console.log('Timeout waiting for image generation'); return null; } // Usage const result = await pollImageJob('YOUR_API_KEY', '4d82d040-4394-4371-867d-72ae1dd62b6d'); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) func pollImageJob(apiKey, jobID string, maxWait, pollInterval time.Duration) (map[string]interface{}, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) startTime := time.Now() for time.Since(startTime) < maxWait { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) success, _ := data["success"].(bool) if !success { fmt.Println("Job failed") return data, nil } jobData, _ := data["data"].(map[string]interface{}) status, _ := jobData["status"].(string) if status == "in-progress" { fmt.Println("Image generation in progress...") time.Sleep(pollInterval) continue } if status == "completed" { fmt.Println("Image generated successfully!") if url, ok := jobData["url"].(string); ok { fmt.Printf("Image URL: %s\n", url) } return data, nil } if status == "failed" { if msg, ok := jobData["error_message"].(string); ok { fmt.Printf("Image generation failed: %s\n", msg) } return data, nil } fmt.Printf("Unexpected status: %s\n", status) return data, nil } return nil, fmt.Errorf("timeout waiting for image generation") } func main() { result, err := pollImageJob( "YOUR_API_KEY", "4d82d040-4394-4371-867d-72ae1dd62b6d", 5*time.Minute, 15*time.Second, ) if err != nil { fmt.Println("Error:", err) return } if success, ok := result["success"].(bool); ok && success { data := result["data"].(map[string]interface{}) if imageURL, ok := data["url"].(string); ok { fmt.Printf("\nImage: %s\n", imageURL) } } } ``` *** ## Polling Best Practices Use a polling interval of **10–30 seconds** when checking job status. Polling too frequently may result in rate limiting. 1. **Use webhooks when possible.** Configure a `webhook` URL in the original generate image request to receive automatic notification when the job completes, rather than polling. 2. **Implement timeouts.** Set a reasonable maximum wait time. Image generation typically completes within a few minutes. 3. **Handle failures gracefully.** Inspect `error_code` and `error_message` in failed responses to determine the cause and whether the request can be retried. # Get Generated Images Source: https://docs.pictory.ai/api-reference/ai-studio/get-images GET https://api.pictory.ai/pictoryapis/v1/aistudio/images Retrieve a paginated list of all AI-generated images ## Overview This endpoint retrieves a paginated list of all AI-generated images associated with your account. The results are sorted by creation date in descending order, with the most recent images returned first. Each item includes the image URL, prompt, model, dimensions, and other metadata from the original generation request. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/aistudio/images ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters A pagination token returned in the previous response. Pass this value to retrieve the next page of results. Omit this parameter to fetch the first page. *** ## Response ### Success Response (200) An array of generated image objects, sorted by creation date (newest first). Up to 100 items are returned per page. Direct download URL for the generated image file The unique identifier of the generated image The text prompt that was used to generate this image The aspect ratio of the generated image (e.g., `"16:9"`, `"9:16"`, `"1:1"`) ISO 8601 timestamp of when the image was generated The AI model that was used for generation (e.g., `"seedream3.0"`, `"nanobanana"`) Width of the generated image in pixels Height of the generated image in pixels The visual style that was applied, if specified in the original request (e.g., `"photorealistic"`, `"artistic"`) The reference image URL that was provided in the original request, if applicable A pagination token to retrieve the next page of results. This field is only present when additional pages are available. Pass this value as the `nextPageKey` query parameter in the next request. ### Response Examples ```json 200 - Success theme={null} { "items": [ { "url": "https://example.cloudfront.net/images/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/c3d4e5f6-a7b8-9012-cdef-345678901234.png", "id": "c3d4e5f6-a7b8-9012-cdef-345678901234", "prompt": "A modern workspace with an open laptop on a clean desk, illuminated by warm daylight from a nearby window", "aspectRatio": "16:9", "createdDate": "2026-04-04T06:03:38.665Z", "model": "nanobanana", "width": 1024, "height": 1024, "style": "photorealistic", "referenceImageUrl": "https://example.cloudfront.net/images/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/b2c3d4e5-f6a7-8901-bcde-234567890123.png" }, { "url": "https://example.cloudfront.net/images/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/e5f6a7b8-c9d0-1234-efab-567890123456.png", "id": "e5f6a7b8-c9d0-1234-efab-567890123456", "prompt": "A man playing guitar on a beach during sunset", "aspectRatio": "16:9", "createdDate": "2026-03-15T21:26:36.239Z", "model": "seedream3.0", "width": 1280, "height": 720 } ], "nextPageKey": "eyJwYXJ0aXRpb25LZXkiOiJ1c2VyI..." } ``` ```json 200 - Empty Result theme={null} { "items": [] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "message": "Internal Server Error" } ``` *** ## Status Codes | Status Code | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Request processed successfully. The `items` array contains the generated images. An empty array is returned if no images have been generated. | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Response Fields | Field | Type | Description | | ------------------- | ------ | ------------------------------------------------------------------------ | | `url` | string | Direct download URL for the generated image | | `id` | string | Unique identifier of the generated image | | `prompt` | string | The text prompt used to generate the image | | `aspectRatio` | string | Aspect ratio of the image (e.g., `"16:9"`) | | `createdDate` | string | ISO 8601 creation timestamp | | `model` | string | AI model used for generation | | `width` | number | Image width in pixels | | `height` | number | Image height in pixels | | `style` | string | Visual style applied (present only if specified in the original request) | | `referenceImageUrl` | string | Reference image URL (present only if specified in the original request) | *** ## Pagination The API returns up to 100 images per page. If more results are available, the response includes a `nextPageKey` field. To retrieve subsequent pages, pass this token as a query parameter in the next request. **Example paginated request:** ``` GET https://api.pictory.ai/pictoryapis/v1/aistudio/images?nextPageKey=eyJwYXJ0aXRpb25LZXkiOiJ1c2VyI... ``` When no `nextPageKey` is present in the response, you have reached the last page. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key from the [API Access page](https://app.pictory.ai/api-access). ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/aistudio/images' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests def get_all_images(api_key): """ Retrieve all generated images with automatic pagination. """ url = "https://api.pictory.ai/pictoryapis/v1/aistudio/images" headers = { "Authorization": api_key, "accept": "application/json" } all_images = [] next_page_key = None while True: params = {} if next_page_key: params["nextPageKey"] = next_page_key response = requests.get(url, headers=headers, params=params) response.raise_for_status() data = response.json() items = data.get("items", []) all_images.extend(items) print(f"Fetched {len(items)} images (total: {len(all_images)})") next_page_key = data.get("nextPageKey") if not next_page_key: break return all_images # Usage images = get_all_images("YOUR_API_KEY") for image in images: print(f"{image['createdDate']} - {image['model']} - {image['url']}") ``` ```javascript JavaScript theme={null} async function getAllImages(apiKey) { const url = 'https://api.pictory.ai/pictoryapis/v1/aistudio/images'; const allImages = []; let nextPageKey = null; while (true) { const params = new URLSearchParams(); if (nextPageKey) { params.set('nextPageKey', nextPageKey); } const requestUrl = params.toString() ? `${url}?${params.toString()}` : url; const response = await fetch(requestUrl, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); const items = data.items || []; allImages.push(...items); console.log(`Fetched ${items.length} images (total: ${allImages.length})`); nextPageKey = data.nextPageKey; if (!nextPageKey) break; } return allImages; } // Usage const images = await getAllImages('YOUR_API_KEY'); images.forEach(img => { console.log(`${img.createdDate} - ${img.model} - ${img.url}`); }); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" ) func getAllImages(apiKey string) ([]map[string]interface{}, error) { baseURL := "https://api.pictory.ai/pictoryapis/v1/aistudio/images" var allImages []map[string]interface{} nextPageKey := "" for { requestURL := baseURL if nextPageKey != "" { requestURL = fmt.Sprintf("%s?nextPageKey=%s", baseURL, url.QueryEscape(nextPageKey)) } req, _ := http.NewRequest("GET", requestURL, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) items, _ := data["items"].([]interface{}) for _, item := range items { if img, ok := item.(map[string]interface{}); ok { allImages = append(allImages, img) } } fmt.Printf("Fetched %d images (total: %d)\n", len(items), len(allImages)) if key, ok := data["nextPageKey"].(string); ok && key != "" { nextPageKey = key } else { break } } return allImages, nil } func main() { images, err := getAllImages("YOUR_API_KEY") if err != nil { fmt.Println("Error:", err) return } for _, img := range images { fmt.Printf("%s - %s - %s\n", img["createdDate"], img["model"], img["url"]) } } ``` # Get Video Generation Job Source: https://docs.pictory.ai/api-reference/ai-studio/get-video-job GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} Retrieve the status and output of an AI video generation job ## Overview This endpoint retrieves the current status and output of an AI video generation job using its unique job ID. While the generation is in progress, it returns the job status. Once the video generation completes, it returns the output including the video URL, thumbnail images, dimensions, duration, and AI credits consumed. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the video generation job. This is the `jobId` returned by the [Generate Video](/api-reference/ai-studio/generate-video) endpoint. **Example:** `"4a09edf9-d071-4847-bda5-8dea913d80fe"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response ### In-Progress Response Returned while the video is being generated: The unique identifier of the video generation job `true` while the job is processing `"in-progress"` while the video is still being generated ### Completed Response Returned when the video has been generated successfully: The unique identifier of the video generation job `true` when the job has completed successfully Contains the video generation output. `"completed"` when the video has been generated successfully Direct download URL for the generated video file (MP4) Duration of the generated video (e.g., `"8s"`) Width of the generated video in pixels Height of the generated video in pixels URL for the video thumbnail image (JPG) URL for the last frame of the video (PNG). This can be used as a `firstFrameImageUrl` in a subsequent generate video request to create a seamless continuation. URL for the video preview image (JPG) Number of AI credits consumed for this video generation ### Failed Response Returned when the video generation job has failed: The unique identifier of the video generation job `false` when the job has failed `"failed"` when the video generation has failed Error code identifying the failure type (e.g., `"5001"`) Descriptive message explaining the cause of the failure ### Response Examples ```json 200 - In Progress theme={null} { "job_id": "4a09edf9-d071-4847-bda5-8dea913d80fe", "success": true, "data": { "status": "in-progress" } } ``` ```json 200 - Completed theme={null} { "job_id": "4a09edf9-d071-4847-bda5-8dea913d80fe", "success": true, "data": { "duration": "8s", "thumbnailImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_thumbnail.jpg", "lastFrameImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_last_frame.png", "previewImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_preview.jpg", "width": 720, "aiCreditsUsed": 12.8, "height": 1280, "url": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654.mp4", "status": "completed" } } ``` ```json 200 - Failed theme={null} { "job_id": "4a09edf9-d071-4847-bda5-8dea913d80fe", "success": false, "data": { "status": "failed", "error_code": "5001", "error_message": "Video generation failed due to an internal error." } } ``` ```json 200 - Invalid Job ID theme={null} { "id": "4a09edf9-d071-4847-bda5-8dea913d80fe", "success": false, "data": { "error_code": "5000", "error_message": "JOB_NOT_FOUND" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Status Codes | Status Code | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Request processed successfully. Check the `status` field in `data` to determine the job state (`in-progress`, `completed`, or `failed`). | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **404** | Job not found. The provided `jobid` does not match any existing job. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Completed Response Fields | Field | Description | | ------------------- | ---------------------------------------------------------------------- | | `url` | Direct download link for the generated MP4 video file | | `duration` | Duration of the generated video (e.g., `"8s"`) | | `width` | Width of the generated video in pixels | | `height` | Height of the generated video in pixels | | `thumbnailImageUrl` | Auto-generated thumbnail image for the video (JPG) | | `lastFrameImageUrl` | Last frame of the video (PNG), useful for creating video continuations | | `previewImageUrl` | Preview image for the video (JPG) | | `aiCreditsUsed` | Number of AI credits consumed for this generation | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and use the `jobId` returned from the [Generate Video](/api-reference/ai-studio/generate-video) endpoint. ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/4a09edf9-d071-4847-bda5-8dea913d80fe' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests import time def poll_video_job(api_key, job_id, max_wait=600, poll_interval=15): """ Poll for video generation job completion. Args: api_key: Your Pictory API key job_id: The video generation job ID max_wait: Maximum wait time in seconds (default 10 minutes) poll_interval: Polling interval in seconds (default 15 seconds) """ url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}" headers = { "Authorization": api_key, "accept": "application/json" } start_time = time.time() while time.time() - start_time < max_wait: response = requests.get(url, headers=headers) data = response.json() if not data.get("success"): error_data = data.get("data", {}) print(f"Job failed: {error_data.get('error_message', 'Unknown error')}") return data status = data.get("data", {}).get("status") if status == "in-progress": print("Video generation in progress...") time.sleep(poll_interval) continue if status == "completed": result = data["data"] print(f"Video generated successfully!") print(f"Video URL: {result.get('url')}") print(f"Duration: {result.get('duration')}") print(f"Dimensions: {result.get('width')}x{result.get('height')}") print(f"AI Credits Used: {result.get('aiCreditsUsed')}") return data if status == "failed": error_msg = data["data"].get("error_message", "Unknown error") print(f"Video generation failed: {error_msg}") return data print(f"Unexpected status: {status}") return data print("Timeout waiting for video generation") return None # Usage result = poll_video_job("YOUR_API_KEY", "4a09edf9-d071-4847-bda5-8dea913d80fe") ``` ```javascript JavaScript theme={null} async function pollVideoJob(apiKey, jobId, maxWait = 600000, pollInterval = 15000) { const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`; const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); if (!data.success) { console.log(`Job failed: ${data.data?.error_message || 'Unknown error'}`); return data; } const status = data.data?.status; if (status === 'in-progress') { console.log('Video generation in progress...'); await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } if (status === 'completed') { console.log('Video generated successfully!'); console.log(`Video URL: ${data.data.url}`); console.log(`Duration: ${data.data.duration}`); console.log(`Dimensions: ${data.data.width}x${data.data.height}`); console.log(`AI Credits Used: ${data.data.aiCreditsUsed}`); return data; } if (status === 'failed') { console.log(`Video generation failed: ${data.data?.error_message}`); return data; } console.log(`Unexpected status: ${status}`); return data; } console.log('Timeout waiting for video generation'); return null; } // Usage const result = await pollVideoJob('YOUR_API_KEY', '4a09edf9-d071-4847-bda5-8dea913d80fe'); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) func pollVideoJob(apiKey, jobID string, maxWait, pollInterval time.Duration) (map[string]interface{}, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) startTime := time.Now() for time.Since(startTime) < maxWait { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) success, _ := data["success"].(bool) if !success { fmt.Println("Job failed") return data, nil } jobData, _ := data["data"].(map[string]interface{}) status, _ := jobData["status"].(string) if status == "in-progress" { fmt.Println("Video generation in progress...") time.Sleep(pollInterval) continue } if status == "completed" { fmt.Println("Video generated successfully!") if videoURL, ok := jobData["url"].(string); ok { fmt.Printf("Video URL: %s\n", videoURL) } if duration, ok := jobData["duration"].(string); ok { fmt.Printf("Duration: %s\n", duration) } return data, nil } if status == "failed" { if msg, ok := jobData["error_message"].(string); ok { fmt.Printf("Video generation failed: %s\n", msg) } return data, nil } fmt.Printf("Unexpected status: %s\n", status) return data, nil } return nil, fmt.Errorf("timeout waiting for video generation") } func main() { result, err := pollVideoJob( "YOUR_API_KEY", "4a09edf9-d071-4847-bda5-8dea913d80fe", 10*time.Minute, 15*time.Second, ) if err != nil { fmt.Println("Error:", err) return } if success, ok := result["success"].(bool); ok && success { data := result["data"].(map[string]interface{}) if videoURL, ok := data["url"].(string); ok { fmt.Printf("\nVideo: %s\n", videoURL) } } } ``` *** ## Polling Best Practices Use a polling interval of **10–30 seconds** when checking job status. Polling too frequently may result in rate limiting. 1. **Use webhooks when possible.** Configure a `webhook` URL in the original generate video request to receive automatic notification when the job completes, rather than polling. 2. **Implement timeouts.** Set a reasonable maximum wait time. Video generation can take several minutes depending on the model and duration selected. 3. **Handle failures gracefully.** Inspect `error_code` and `error_message` in failed responses to determine the cause and whether the request can be retried. 4. **Use the last frame for continuations.** The `lastFrameImageUrl` from a completed job can be passed as `firstFrameImageUrl` in a new generate video request to create a seamless video continuation. # Get Generated Videos Source: https://docs.pictory.ai/api-reference/ai-studio/get-videos GET https://api.pictory.ai/pictoryapis/v1/aistudio/videos Retrieve a paginated list of all AI-generated videos ## Overview This endpoint retrieves a paginated list of all AI-generated videos associated with your account. The results are sorted by creation date in descending order, with the most recent videos returned first. Each item includes the video URL, prompt, model, dimensions, duration, thumbnail images, and other metadata from the original generation request. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/aistudio/videos ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters A pagination token returned in the previous response. Pass this value to retrieve the next page of results. Omit this parameter to fetch the first page. *** ## Response ### Success Response (200) An array of generated video objects, sorted by creation date (newest first). Up to 100 items are returned per page. Direct download URL for the generated video file (MP4) The unique identifier of the generated video The text prompt that was used to generate this video The aspect ratio of the generated video (e.g., `"16:9"`, `"9:16"`, `"1:1"`) ISO 8601 timestamp of when the video was generated The AI model that was used for generation (e.g., `"pixverse5.5"`, `"veo3.1"`) Width of the generated video in pixels Height of the generated video in pixels Duration of the generated video (e.g., `"8s"`) URL for the video preview image (JPG) URL for the video thumbnail image (JPG) URL for the last frame of the video (PNG). This can be used as a `firstFrameImageUrl` in a subsequent generation request. The first frame image URL that was provided in the original request, if applicable The reference image URLs that were provided in the original request, if applicable The source video URL that was extended, if applicable A pagination token to retrieve the next page of results. This field is only present when additional pages are available. Pass this value as the `nextPageKey` query parameter in the next request. ### Response Examples ```json 200 - Success theme={null} { "items": [ { "url": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654.mp4", "id": "20260409185104635ec00b33280fc451f", "prompt": "A woman sitting on the grass as children run and play around her in a wide shot", "aspectRatio": "9:16", "createdDate": "2026-04-09T18:51:47.967Z", "model": "pixverse5.5", "width": 720, "height": 1280, "duration": "8s", "previewImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_preview.jpg", "thumbnailImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_thumbnail.jpg", "lastFrameImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/d7c6b5a4-e3f2-1098-dcba-fedcba987654_last_frame.png" }, { "url": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/f8e7d6c5-b4a3-2190-fedc-ba0987654321.mp4", "id": "2026040918025436554deba0b339b4a01", "prompt": "A woman walking through an open field towards a farmhouse in the distance in a wide shot", "aspectRatio": "9:16", "createdDate": "2026-04-09T18:02:57.039Z", "model": "pixverse5.5", "width": 720, "height": 1280, "duration": "8s", "previewImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/f8e7d6c5-b4a3-2190-fedc-ba0987654321_preview.jpg", "thumbnailImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/f8e7d6c5-b4a3-2190-fedc-ba0987654321_thumbnail.jpg", "lastFrameImageUrl": "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/f8e7d6c5-b4a3-2190-fedc-ba0987654321_last_frame.png", "referenceImageUrls": [ "https://example.cloudfront.net/videos/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/a9b8c7d6-e5f4-3210-abcd-ef9876543210_last_frame.png", "https://example.cloudfront.net/images/user/a1b2c3d4-e5f6-7890-abcd-ef1234567890/1a2b3c4d-5e6f-7890-abcd-ef0123456789.png" ] } ], "nextPageKey": "eyJwYXJ0aXRpb25LZXkiOiJ1c2VyI..." } ``` ```json 200 - Empty Result theme={null} { "items": [] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "message": "Internal Server Error" } ``` *** ## Status Codes | Status Code | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **200** | Request processed successfully. The `items` array contains the generated videos. An empty array is returned if no videos have been generated. | | **401** | Unauthorized. The API key in the `Authorization` header is missing or invalid. | | **500** | Internal server error. Retry the request after a brief delay. | *** ## Response Fields | Field | Type | Always Present | Description | | -------------------- | ------ | -------------- | ------------------------------------------------------------------------ | | `url` | string | Yes | Direct download URL for the generated video (MP4) | | `id` | string | Yes | Unique identifier of the generated video | | `prompt` | string | Yes | The text prompt used to generate the video | | `aspectRatio` | string | Yes | Aspect ratio of the video (e.g., `"9:16"`) | | `createdDate` | string | Yes | ISO 8601 creation timestamp | | `model` | string | Yes | AI model used for generation | | `width` | number | Yes | Video width in pixels | | `height` | number | Yes | Video height in pixels | | `duration` | string | Yes | Video duration (e.g., `"8s"`) | | `previewImageUrl` | string | Yes | Preview image URL (JPG) | | `thumbnailImageUrl` | string | Yes | Thumbnail image URL (JPG) | | `lastFrameImageUrl` | string | Yes | Last frame image URL (PNG), useful for creating video continuations | | `firstFrameImageUrl` | string | No | Present only if a first frame image was provided in the original request | | `referenceImageUrls` | array | No | Present only if reference images were provided in the original request | | `extendVideoUrl` | string | No | Present only if the video was generated by extending another video | *** ## Pagination The API returns up to 100 videos per page. If more results are available, the response includes a `nextPageKey` field. To retrieve subsequent pages, pass this token as a query parameter in the next request. **Example paginated request:** ``` GET https://api.pictory.ai/pictoryapis/v1/aistudio/videos?nextPageKey=eyJwYXJ0aXRpb25LZXkiOiJ1c2VyI... ``` When no `nextPageKey` is present in the response, you have reached the last page. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key from the [API Access page](https://app.pictory.ai/api-access). ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/aistudio/videos' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests def get_all_videos(api_key): """ Retrieve all generated videos with automatic pagination. """ url = "https://api.pictory.ai/pictoryapis/v1/aistudio/videos" headers = { "Authorization": api_key, "accept": "application/json" } all_videos = [] next_page_key = None while True: params = {} if next_page_key: params["nextPageKey"] = next_page_key response = requests.get(url, headers=headers, params=params) response.raise_for_status() data = response.json() items = data.get("items", []) all_videos.extend(items) print(f"Fetched {len(items)} videos (total: {len(all_videos)})") next_page_key = data.get("nextPageKey") if not next_page_key: break return all_videos # Usage videos = get_all_videos("YOUR_API_KEY") for video in videos: print(f"{video['createdDate']} - {video['model']} - {video['duration']} - {video['url']}") ``` ```javascript JavaScript theme={null} async function getAllVideos(apiKey) { const url = 'https://api.pictory.ai/pictoryapis/v1/aistudio/videos'; const allVideos = []; let nextPageKey = null; while (true) { const params = new URLSearchParams(); if (nextPageKey) { params.set('nextPageKey', nextPageKey); } const requestUrl = params.toString() ? `${url}?${params.toString()}` : url; const response = await fetch(requestUrl, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); const items = data.items || []; allVideos.push(...items); console.log(`Fetched ${items.length} videos (total: ${allVideos.length})`); nextPageKey = data.nextPageKey; if (!nextPageKey) break; } return allVideos; } // Usage const videos = await getAllVideos('YOUR_API_KEY'); videos.forEach(video => { console.log(`${video.createdDate} - ${video.model} - ${video.duration} - ${video.url}`); }); ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" ) func getAllVideos(apiKey string) ([]map[string]interface{}, error) { baseURL := "https://api.pictory.ai/pictoryapis/v1/aistudio/videos" var allVideos []map[string]interface{} nextPageKey := "" for { requestURL := baseURL if nextPageKey != "" { requestURL = fmt.Sprintf("%s?nextPageKey=%s", baseURL, url.QueryEscape(nextPageKey)) } req, _ := http.NewRequest("GET", requestURL, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) items, _ := data["items"].([]interface{}) for _, item := range items { if video, ok := item.(map[string]interface{}); ok { allVideos = append(allVideos, video) } } fmt.Printf("Fetched %d videos (total: %d)\n", len(items), len(allVideos)) if key, ok := data["nextPageKey"].(string); ok && key != "" { nextPageKey = key } else { break } } return allVideos, nil } func main() { videos, err := getAllVideos("YOUR_API_KEY") if err != nil { fmt.Println("Error:", err) return } for _, video := range videos { fmt.Printf("%s - %s - %s - %s\n", video["createdDate"], video["model"], video["duration"], video["url"]) } } ``` # Get Avatars Source: https://docs.pictory.ai/api-reference/avatars/get-avatars GET https://api.pictory.ai/pictoryapis/v1/avatars Retrieve the list of available AI avatars and their looks for video creation ## Overview The Get Avatars API retrieves a paginated list of all available AI avatars organized by avatar groups. Each avatar group contains multiple "looks" (different styles/outfits) that you can use to create presenter-style videos. Use this endpoint to discover available avatars before creating avatar videos. The look `id` becomes your `avatarId` when using the [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) API. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/avatars ``` *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` *** ## Query Parameters Base64 pagination token returned in `nextPageKey` from previous response. Omit for first page. *** ## Response The API returns avatar groups with their available looks. Array of avatar group objects Base64 pagination token for next page. `null` indicates no more pages available. ### Avatar Group Object (`items[]`) Unique avatar group ID (e.g., "e0e84faea390465896db75a83be45085") Display name of the avatar person (e.g., "Annie", "Brandon"). Gender of the avatar: `"Male"`, `"Female"`, or `null` Array of available looks/styles for this avatar person ### Avatar Look Object (`items[].looks[]`) Unique look identifier (e.g., "Annie\_expressive12\_public"). Use this as `avatarId` in video creation. Display name of the look (e.g., "Annie in Tan Jacket", "Brandon in Grey Suit") URL to static preview image (.webp format) URL to blurred preview video (.webm format) showing the avatar in action *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/avatars' \ --header 'Authorization: YOUR_API_KEY' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/avatars', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY' } }); const data = await response.json(); console.log('Avatar groups:', data.items.length); console.log('Next page key:', data.nextPageKey); ``` ```python Python theme={null} import requests response = requests.get( 'https://api.pictory.ai/pictoryapis/v1/avatars', headers={ 'Authorization': 'YOUR_API_KEY' } ) data = response.json() print(f"Avatar groups: {len(data['items'])}") print(f"Next page key: {data['nextPageKey']}") ``` *** ## Response Example ```json 200 - Success theme={null} { "items": [ { "id": "e0e84faea390465896db75a83be45085", "name": "Annie", "gender": "Female", "looks": [ { "id": "Annie_expressive12_public", "name": "Annie in Tan Jacket", "previewImage": "https://pictory-static.pictorycontent.com/avatars/preview/e0e84faea390465896db75a83be45085/Annie_expressive12_public/previewImage.webp", "previewVideo": "https://pictory-static.pictorycontent.com/avatars/preview/e0e84faea390465896db75a83be45085/Annie_expressive12_public/previewVideo_blurred.webm" }, { "id": "Annie_expressive2_public", "name": "Annie in Blue Casual", "previewImage": "https://pictory-static.pictorycontent.com/avatars/preview/e0e84faea390465896db75a83be45085/Annie_expressive2_public/previewImage.webp", "previewVideo": "https://pictory-static.pictorycontent.com/avatars/preview/e0e84faea390465896db75a83be45085/Annie_expressive2_public/previewVideo_blurred.webm" } ] }, { "id": "d08c85e6cff84d78b6dc41d83a2eccce", "name": "Brandon", "gender": "Male", "looks": [ { "id": "Brandon_expressive2_public", "name": "Brandon in Grey Suit", "previewImage": "https://pictory-static.pictorycontent.com/avatars/preview/d08c85e6cff84d78b6dc41d83a2eccce/Brandon_expressive2_public/previewImage.webp", "previewVideo": "https://pictory-static.pictorycontent.com/avatars/preview/d08c85e6cff84d78b6dc41d83a2eccce/Brandon_expressive2_public/previewVideo_blurred.webm" } ] } ], "nextPageKey": null } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Pagination The API returns results in pages. Use the `nextPageKey` to fetch subsequent pages: ```javascript theme={null} // Fetch first page let response = await fetch('https://api.pictory.ai/pictoryapis/v1/avatars', { headers: { 'Authorization': 'YOUR_API_KEY' } }); let data = await response.json(); let allAvatars = [...data.items]; // Fetch remaining pages while (data.nextPageKey) { response = await fetch(`https://api.pictory.ai/pictoryapis/v1/avatars?nextPageKey=${encodeURIComponent(data.nextPageKey)}`, { headers: { 'Authorization': 'YOUR_API_KEY' } }); data = await response.json(); allAvatars.push(...data.items); } console.log('Total avatar groups:', allAvatars.length); ``` *** ## Usage Example Here's how to fetch avatars and use them in video creation: ```javascript theme={null} // Step 1: Get available avatars const avatarsResponse = await fetch('https://api.pictory.ai/pictoryapis/v1/avatars', { headers: { 'Authorization': 'YOUR_API_KEY' } }); const avatarsData = await avatarsResponse.json(); // Step 2: Find an avatar group by name const annieGroup = avatarsData.items.find(group => group.name === 'Annie'); // Step 3: Select a specific look const selectedLook = annieGroup.looks.find(look => look.name.includes('Tan Jacket')); console.log('Selected avatar:'); console.log(' avatarId:', selectedLook.id); console.log(' Preview:', selectedLook.previewVideo); // Step 4: Create video with the selected avatar const videoResponse = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_avatar_video', language: 'en', videoWidth: 1920, videoHeight: 1080, voiceOver: { enabled: true, aiVoices: [{ speaker: 'Matthew', speed: 100 }] }, avatar: { avatarId: selectedLook.id, // "Annie_expressive12_public" position: 'bottom-right', width: '20%' }, scenes: [{ story: 'Welcome to my video!', createSceneOnEndOfSentence: true, minimumDuration: 4 }] }) }); const videoData = await videoResponse.json(); console.log('Video job created:', videoData.data.jobId); ``` *** ## Filtering and Selecting Avatars You can filter avatars based on your requirements: ```javascript theme={null} const avatarsData = await getAvatars(); // Filter by gender const femaleAvatars = avatarsData.items.filter(group => group.gender === 'Female'); const maleAvatars = avatarsData.items.filter(group => group.gender === 'Male'); // Get all looks for female avatars const femaleLooks = femaleAvatars.flatMap(group => group.looks.map(look => ({ avatarId: look.id, lookName: look.name, gender: group.gender })) ); // Find specific avatar by name const caroline = avatarsData.items.find(group => group.name === 'Caroline'); const carolineLooks = caroline ? caroline.looks : []; // Search for looks by outfit const suitLooks = avatarsData.items.flatMap(group => group.looks.filter(look => look.name.toLowerCase().includes('suit')) ); console.log('Female avatars:', femaleAvatars.length); console.log('Male avatars:', maleAvatars.length); console.log('Suit looks:', suitLooks.length); ``` *** ## Best Practices **Recommendation:** Cache the avatar list to reduce API calls The avatar list does not change frequently, so you can cache it: * Store the response locally or in your database * Refresh the cache periodically (e.g., daily or weekly) * Reduce latency and API calls in your application * Remember to handle pagination when caching **Recommendation:** Fetch all pages for complete avatar list The API returns paginated results: * Check `nextPageKey` for additional pages * Continue fetching until `nextPageKey` is `null` * Combine results from all pages * Cache the complete list after fetching all pages **Recommendation:** Show preview videos to users Use the `previewVideo` and `previewImage` URLs to: * Display avatar previews in your UI * Let users see avatar appearance and style * Show different looks for the same avatar person * Help users make informed selections **Recommendation:** Check avatar availability before creating videos Before hardcoding avatar IDs: * Fetch the latest avatar list * Verify the look ID exists * Handle cases where a look might be deprecated * Provide fallback options **Recommendation:** Group vs Look distinction matters Remember the hierarchy: * **Avatar Group** (`items[]`): The avatar person (e.g., "Annie") * **Avatar Look** (`items[].looks[]`): A specific style/outfit (e.g., "Annie in Tan Jacket") * Use `items[].looks[].id` as `avatarId` in video creation One avatar person can have multiple looks to choose from. *** ## Error Handling ```json theme={null} { "message": "Unauthorized" } ``` **Solution:** Check your API key is valid and correctly formatted in the Authorization header. **Issue:** Providing an invalid or expired `nextPageKey` parameter **Solution:** * Only use `nextPageKey` values returned from previous API responses * Do not manually construct or modify page keys * If pagination fails, restart from the first page **Issue:** Request fails or times out **Solution:** * Check network connectivity * Verify the endpoint URL is correct * Implement retry logic with exponential backoff * Handle timeout errors gracefully *** ## Related Resources Create presenter-style videos with avatars Step-by-step guide to creating avatar videos Learn to position avatars in videos Get available AI voices for avatar narration # Create AWS Connection for Private S3 Assets Source: https://docs.pictory.ai/api-reference/aws-integration/aws-private-connection POST https://api.pictory.ai/pictoryapis/v1/awsconnections Connect your AWS account to access private S3 videos and images in Pictory ## Overview This guide shows you how to connect your private AWS S3 storage to Pictory, so you can use your own videos and images stored in Amazon S3 buckets to create Pictory videos. **What you will accomplish:** * Connect Pictory to your private AWS S3 storage * Use your private videos and images in Pictory without making them public * Keep your assets secure with AWS IAM role-based access **Prerequisites:** You will need an AWS account with access to create IAM roles. If you do not have AWS experience, consider asking your IT team for help with the AWS setup steps below. *** ## Prerequisites: AWS IAM Role Setup This creates a secure "key" (called an IAM role) that lets Pictory access your private S3 files without making them public. ### How It Works Think of this like giving Pictory a guest pass to your storage: 1. You create a special role in AWS (the "guest pass") 2. You tell AWS that Pictory is allowed to use this role 3. You specify which folders/buckets Pictory can access 4. Pictory uses this role to fetch your videos and images when creating content 1) Go to [AWS Management Console](https://console.aws.amazon.com/) 2) Sign in with your AWS account credentials 3) In the search bar at the top, type "IAM" and click on the **IAM** service **What is IAM?** IAM (Identity and Access Management) is AWS's security system that controls who can access what in your AWS account. 1. On the left sidebar, click **Roles** 2. Click the orange **Create role** button **What is a Role?** A role is like a job title with specific permissions. You're creating a "Pictory Access" role that can only read your S3 files. This step tells AWS that Pictory's account is allowed to use this role. 1. Under **Trusted entity type**, select **Another AWS account** 2. In the **Account ID** field, enter: `701488979254` (this is Pictory's AWS account) 3. Click **Next** 4. On the permissions page, click **Next** (we will add permissions in the next step) 5. Under **Role name**, enter exactly: `PictoryCloudIntegrationRole` (this exact name is required) 6. Click **Create role** This step specifies which S3 bucket Pictory can access and what it can do (read files and list files). 1. In the search box, find the role you just created: `PictoryCloudIntegrationRole` 2. Click on it to open the role details 3. Click the **Permissions** tab 4. Click **Add permissions** → **Create inline policy** 5. Click the **JSON** tab (ignore the visual editor) 6. Delete everything in the box and paste this: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:ListBucket", "s3:GetObject" ], "Resource": [ "arn:aws:s3:::{YOUR_S3_BUCKET}", "arn:aws:s3:::{YOUR_S3_BUCKET}/*" ] }, { "Effect": "Allow", "Action": [ "s3:ListAllMyBuckets" ], "Resource": [ "arn:aws:s3:::*" ] } ] } ``` 7. **Important:** Replace `{YOUR_S3_BUCKET}` with your actual bucket name (for example, if your bucket is named "my-company-videos", replace both instances with "my-company-videos") 8. Click **Next** 9. Name the policy: `s3_access_policy` 10. Click **Create policy** **What this does:** * `s3:ListBucket` - Lets Pictory see what files are in your bucket * `s3:GetObject` - Lets Pictory read/download the files * Pictory can only READ files, it cannot modify or delete them This step sets up the detailed trust relationship between your AWS account and Pictory. 1. In the role details, click the **Trust relationships** tab 2. Click **Edit trust policy** 3. Delete everything and paste this: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::701488979254:role/CloudIntegrationRole" }, "Action": "sts:AssumeRole", "Condition": {} }, { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::701488979254:role/ecsTaskExecutionRole" }, "Action": "sts:AssumeRole", "Condition": {} }, { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::701488979254:root" }, "Action": "sts:AssumeRole", "Condition": {} } ] } ``` 4. Click **Update policy** **What this does:** This lists three Pictory systems that are allowed to use this role: * `CloudIntegrationRole` - Handles the API connection * `ecsTaskExecutionRole` - Processes your videos * `root` - Backup access for Pictory's account You'll need these two pieces of information to make the API call: **Your AWS Account ID:** * In the AWS Console, click your account name in the top-right corner * Your 12-digit Account ID is shown there (for example: `123456789012`) **Your S3 Bucket's Region:** * Go to the S3 service in AWS Console * Find your bucket in the list * The region is shown next to the bucket name (for example: `us-east-1`, `us-west-2`, etc.) Write these down - you will use them in the next section when making the API call! *** ## Making the API Call Now that your AWS role is set up, you can make a simple API call to connect it to Pictory. **What you will need:** * Your API key (starts with `pictai_` - get this from the [API Access page](https://app.pictory.ai/api-access)) * Your 12-digit AWS Account ID (from Step 6 above) * Your S3 bucket's region (from Step 6 above) ### API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/awsconnections ``` *** ## Request Parameters **For non-technical users:** The sections below show what information you need to include in your API request. If you are using a tool like Postman or writing code, these are the fields you will fill in. ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. Must be set to `application/json` ### Body Parameters A unique name for the AWS connection **Example:** `"PictoryPrivateVideosConnection"` Optional description of the AWS connection **Example:** `"Pictory Private Videos Connection"` Your 12-digit AWS account ID **Format:** 12-digit numeric string **Example:** `"123456789012"` The AWS region where your S3 bucket is located **Common Regions:** * `us-east-1` - US East (N. Virginia) * `us-east-2` - US East (Ohio) * `us-west-1` - US West (N. California) * `us-west-2` - US West (Oregon) * `eu-west-1` - Europe (Ireland) * `eu-central-1` - Europe (Frankfurt) * `ap-southeast-1` - Asia Pacific (Singapore) * `ap-northeast-1` - Asia Pacific (Tokyo) **Example:** `"us-east-2"` Whether the AWS connection is enabled (should be `true` to activate the connection) **Default:** `true` ### Request Body Example Here's what the complete request looks like. Replace the example values with your actual AWS details: ```json theme={null} { "name": "PictoryPrivateVideosConnection", "description": "Pictory Private Videos Connection", "awsAccountId": "123456789012", ← Replace with your 12-digit AWS Account ID "awsRegion": "us-east-2", ← Replace with your S3 bucket's region "enabled": true } ``` **Quick tip:** You can leave out the `description` field if you do not need it - it is optional! *** ## Response When the connection is successful, Pictory will send back a response confirming the details. **Save your connectionId!** You'll use this ID when making video API calls to tell Pictory which S3 connection to use. When using it in video creation requests, the field name becomes `awsConnectionId` (not just `connectionId`). ### Response Examples ```json 201 - Created theme={null} { "enabled": true, "name": "PictoryPrivateVideosConnection", "description": "Pictory Private Videos Connection", "awsAccountId": "123456789012", "awsRegion": "us-east-2", "connectionId": "20241207042423053xmex5z9ag9ivmp21", "version": 1 } ``` ```json 400 - Bad Request (Invalid AWS Account ID) theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "awsAccountId", "errors": "AWS account ID must be a 12-digit numeric string" } ] } ``` ```json 400 - Bad Request (Missing Required Field) theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "enabled", "errors": "enabled is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 409 - Conflict (Duplicate Connection) theme={null} { "code": "DUPLICATE_CONNECTION", "message": "Connection with same name or account/region already exist.", "fields": [] } ``` *** ## Code Examples Here are complete working examples in different programming languages. Pick the one that matches your programming language. **New to APIs?** If these code examples look confusing, consider using a tool like [Postman](https://www.postman.com/) or [Insomnia](https://insomnia.rest/) - they provide a visual interface where you can just fill in fields instead of writing code. ```bash cURL theme={null} # This is for terminal/command line use # Replace YOUR_API_KEY with your actual API key # Replace 123456789012 with your AWS Account ID # Replace us-east-2 with your S3 bucket's region curl --request POST \ --url https://api.pictory.ai/pictoryapis/v1/awsconnections \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' \ --header 'content-type: application/json' \ --data '{ "name": "PictoryPrivateVideosConnection", "description": "Pictory Private Videos Connection", "awsAccountId": "123456789012", "awsRegion": "us-east-2", "enabled": true }' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // For use in web applications or Node.js // Replace 'YOUR_API_KEY' with your actual API key // Replace '123456789012' with your AWS Account ID // Replace 'us-east-2' with your S3 bucket's region const createAWSConnection = async (apiKey) => { const response = await fetch('https://api.pictory.ai/pictoryapis/v1/awsconnections', { method: 'POST', headers: { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'PictoryPrivateVideosConnection', description: 'Pictory Private Videos Connection', awsAccountId: '123456789012', // ← Replace with your AWS Account ID awsRegion: 'us-east-2', // ← Replace with your region enabled: true }) }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // How to use it: try { const connection = await createAWSConnection('YOUR_API_KEY'); console.log('Success! Connection ID:', connection.connectionId); console.log('Save this ID for making video requests!'); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # For Python applications # First install requests: pip install requests # Replace 'YOUR_API_KEY' with your actual API key # Replace '123456789012' with your AWS Account ID # Replace 'us-east-2' with your S3 bucket's region import requests def create_aws_connection(api_key): """Create AWS connection to access private S3 assets""" url = "https://api.pictory.ai/pictoryapis/v1/awsconnections" headers = { "Authorization": api_key, "Content-Type": "application/json" } payload = { "name": "PictoryPrivateVideosConnection", "description": "Pictory Private Videos Connection", "awsAccountId": "123456789012", # ← Replace with your AWS Account ID "awsRegion": "us-east-2", # ← Replace with your region "enabled": True } response = requests.post(url, headers=headers, json=payload) response.raise_for_status() # Raises error if request failed return response.json() # How to use it: try: connection = create_aws_connection("YOUR_API_KEY") print(f"Success! Connection ID: {connection['connectionId']}") print(f"Save this ID for making video requests!") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} 'PictoryPrivateVideosConnection', 'description' => 'Pictory Private Videos Connection', 'awsAccountId' => '123456789012', // ← Replace with your AWS Account ID 'awsRegion' => 'us-east-2', // ← Replace with your region 'enabled' => true ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: ' . $apiKey, 'Content-Type: application/json' ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 201) { $error = json_decode($response, true); throw new Exception($error['message'] ?? 'Request failed'); } return json_decode($response, true); } // How to use it: try { $connection = createAWSConnection('YOUR_API_KEY'); echo "Success! Connection ID: " . $connection['connectionId'] . "\n"; echo "Save this ID for making video requests!\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // For Go applications // Replace "YOUR_API_KEY" with your actual API key // Replace "123456789012" with your AWS Account ID // Replace "us-east-2" with your S3 bucket's region package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type AWSConnectionRequest struct { Name string `json:"name"` Description string `json:"description"` AWSAccountID string `json:"awsAccountId"` AWSRegion string `json:"awsRegion"` Enabled bool `json:"enabled"` } type AWSConnectionResponse struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` AWSAccountID string `json:"awsAccountId"` AWSRegion string `json:"awsRegion"` Enabled bool `json:"enabled"` Version int `json:"version"` } func createAWSConnection(apiKey string) (*AWSConnectionResponse, error) { url := "https://api.pictory.ai/pictoryapis/v1/awsconnections" req := AWSConnectionRequest{ Name: "PictoryPrivateVideosConnection", Description: "Pictory Private Videos Connection", AWSAccountID: "123456789012", // ← Replace with your AWS Account ID AWSRegion: "us-east-2", // ← Replace with your region Enabled: true, } jsonData, err := json.Marshal(req) if err != nil { return nil, err } httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } httpReq.Header.Set("Authorization", apiKey) httpReq.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(httpReq) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result AWSConnectionResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // How to use it: func main() { connection, err := createAWSConnection("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Success! Connection ID: %s\n", connection.ConnectionID) fmt.Println("Save this ID for making video requests!") } ``` *** ## Using Your S3 Assets in Videos Now that you have created the connection, you can use your private S3 files in Pictory videos by including the `awsConnectionId` and referencing files with the `s3://` format. ### Example Video Request Here is a simple example of creating a video using your private S3 files: ```json theme={null} { "awsConnectionId": "20241207042423053xmex5z9ag9ivmp21", ← Use your connectionId here "videoName": "My Marketing Video", "videoDescription": "Company intro video", "language": "en", "scenes": [ { "text": "Welcome to our company!", "backgroundUri": "s3://my-private-bucket/intro.mp4", ← Your private S3 file "backgroundType": "video", "minimumDuration": 5 }, { "text": "We're excited to have you here!", "backgroundUri": "s3://my-private-bucket/office.jpg", ← Private S3 image "backgroundType": "image", "minimumDuration": 5 } ] } ``` ### How to Reference Your S3 Files You can use either of these two formats to point to your S3 files: ``` s3://your-bucket-name/path/to/file.mp4 ``` **Example:** ``` s3://my-company-videos/marketing/intro.mp4 ``` ``` https://your-bucket-name.s3.region-name.amazonaws.com/path/to/file.mp4 ``` **Example:** ``` https://my-company-videos.s3.us-east-2.amazonaws.com/marketing/intro.mp4 ``` Remember to use `awsConnectionId` in your video requests, not `connectionId`. The API response gives you `connectionId`, but you need to rename it to `awsConnectionId` when making video requests. *** ## Troubleshooting Make sure your Account ID is exactly 12 digits (example: `123456789012`) with no spaces or dashes. **Where to find it:** In AWS Console, click your account name in the top-right corner Your API key may be invalid or expired. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. You already have a connection with this name or AWS account/region. Try a different connection name. Check these in order: 1. Role name must be exactly: `PictoryCloudIntegrationRole` 2. Bucket name in your S3 URIs must match your IAM policy 3. Region must match where your S3 bucket is located 4. Trust policy includes all three Pictory roles (from Step 5) *** Access your API key from your Pictory dashboard Create videos using your private S3 assets Retrieve all your configured AWS connections Modify or disable an existing connection Remove an AWS connection Official AWS IAM documentation # Delete AWS Private Connection Source: https://docs.pictory.ai/api-reference/aws-integration/delete-aws-connection DELETE https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} Permanently remove an AWS S3 private connection ## Overview This endpoint allows you to permanently delete an AWS S3 private connection from your Pictory account. Once deleted, the connection cannot be recovered and any video creation workflows using this connection will fail. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. **This action is permanent and cannot be undone.** Before deleting a connection, ensure: * No active video projects are using this connection * You have documented the connection settings if you may need them again * Consider disabling the connection temporarily instead of deleting it *** ## Use Cases Clean up connections that are no longer needed Remove connections for decommissioned AWS accounts Delete old connections after migrating to new AWS infrastructure Remove test or development connections *** ## API Endpoint ```http theme={null} DELETE https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the AWS connection you want to delete. This is the `connectionId` value returned when you created the connection. ``` Example: 20251217080657842fux3au9kh1p0j5s ``` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## Response ### Success Response A successful deletion returns **204 No Content** with an empty response body. This is the standard HTTP status code for successful deletions. **Status Code:** `204 No Content` **Response Body:** None (empty) ### Response Examples ```text 204 - No Content theme={null} (Empty response body) ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Connection not found" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` Replace `YOUR_CONNECTION_ID` with the actual connection ID you want to delete ```bash cURL theme={null} # Delete an AWS connection # Replace YOUR_API_KEY with your actual API key # Replace YOUR_CONNECTION_ID with your connection ID curl --request DELETE \ --url https://api.pictory.ai/pictoryapis/v1/awsconnections/YOUR_CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key // Replace 'YOUR_CONNECTION_ID' with your connection ID const deleteAwsConnection = async (apiKey, connectionId) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/awsconnections/${connectionId}`, { method: 'DELETE', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } // 204 No Content - successful deletion returns no body return { success: true, status: response.status }; }; // Usage try { const connectionId = 'YOUR_CONNECTION_ID'; // ← Replace with your connection ID const result = await deleteAwsConnection('YOUR_API_KEY', connectionId); // ← Replace with your API key console.log('Connection deleted successfully'); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key # Replace 'YOUR_CONNECTION_ID' with your connection ID import requests def delete_aws_connection(api_key, connection_id): """Delete an AWS S3 connection permanently""" url = f"https://api.pictory.ai/pictoryapis/v1/awsconnections/{connection_id}" headers = { "Authorization": api_key } response = requests.delete(url, headers=headers) response.raise_for_status() # 204 No Content - successful deletion returns no body return {"success": True, "status": response.status_code} # Usage try: connection_id = "YOUR_CONNECTION_ID" # ← Replace with your connection ID result = delete_aws_connection("YOUR_API_KEY", connection_id) # ← Replace with your API key print("Connection deleted successfully") except requests.exceptions.HTTPError as e: if e.response.status_code == 404: print("Error: Connection not found") else: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} true, 'status' => $httpCode]; } // Usage try { $connectionId = 'YOUR_CONNECTION_ID'; // ← Replace with your connection ID $result = deleteAwsConnection('YOUR_API_KEY', $connectionId); // ← Replace with your API key echo "Connection deleted successfully\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key // Replace "YOUR_CONNECTION_ID" with your connection ID package main import ( "fmt" "net/http" ) func deleteAwsConnection(apiKey, connectionId string) error { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/awsconnections/%s", connectionId) req, err := http.NewRequest("DELETE", url, nil) if err != nil { return err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { return fmt.Errorf("connection not found") } if resp.StatusCode != http.StatusNoContent { return fmt.Errorf("request failed (status %d)", resp.StatusCode) } // 204 No Content - successful deletion return nil } // Usage func main() { connectionId := "YOUR_CONNECTION_ID" // ← Replace with your connection ID err := deleteAwsConnection("YOUR_API_KEY", connectionId) // ← Replace with your API key if err != nil { panic(err) } fmt.Println("Connection deleted successfully") } ``` *** ## Common Use Cases ### Safe Deletion with Confirmation ```javascript theme={null} // Check connection exists before deleting const safeDelete = async (apiKey, connectionId) => { try { // First, verify the connection exists const connection = await getAwsConnectionById(apiKey, connectionId); console.log(`About to delete: ${connection.name}`); // Confirm with user (in a real app) const confirmed = true; // Replace with actual user confirmation if (confirmed) { const result = await deleteAwsConnection(apiKey, connectionId); console.log('Connection deleted successfully'); } } catch (error) { console.error('Delete failed:', error.message); } }; ``` ### Delete After Migration ```python theme={null} # Delete old connection after creating new one def migrate_connection(api_key, old_connection_id, new_aws_account, new_region): # Create new connection new_connection = create_aws_connection(api_key, { "name": "MigratedConnection", "description": "Migrated to new AWS account", "awsAccountId": new_aws_account, "awsRegion": new_region, "enabled": True }) print(f"New connection created: {new_connection['connectionId']}") # Delete old connection (returns 204 No Content on success) delete_aws_connection(api_key, old_connection_id) print("Old connection deleted successfully") return new_connection['connectionId'] ``` ### Bulk Cleanup ```javascript theme={null} // Delete multiple unused connections const cleanupConnections = async (apiKey, connectionIdsToDelete) => { const results = []; for (const connectionId of connectionIdsToDelete) { try { await deleteAwsConnection(apiKey, connectionId); results.push({ connectionId, status: 'deleted' }); } catch (error) { results.push({ connectionId, status: 'failed', error: error.message }); } } return results; }; // Usage const toDelete = ['connection-id-1', 'connection-id-2', 'connection-id-3']; const results = await cleanupConnections(apiKey, toDelete); console.log('Cleanup results:', results); ``` ### Delete with Audit Trail ```python theme={null} # Delete connection and log the action import logging from datetime import datetime def delete_with_audit(api_key, connection_id, reason): # Get connection details before deletion connection = get_aws_connection_by_id(api_key, connection_id) # Log deletion logging.info(f"Deleting connection: {connection['name']}") logging.info(f"AWS Account: {connection['awsAccountId']}") logging.info(f"Region: {connection['awsRegion']}") logging.info(f"Reason: {reason}") logging.info(f"Timestamp: {datetime.now().isoformat()}") # Perform deletion (returns 204 No Content on success) delete_aws_connection(api_key, connection_id) logging.info("Deletion completed successfully") return True ``` *** ## Error Handling **Cause:** The connection ID does not exist or has already been deleted **Solution:** * Verify the connection ID is correct * Use the [Get AWS Connections](/api-reference/aws-integration/get-aws-connections) endpoint to list all available connections * Check if the connection was already deleted **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** You do not have permission to delete this connection **Solution:** * Verify the connection belongs to your account * Check that your API key has the necessary permissions * Contact support if you believe you should have access **Cause:** The connection is currently being used by active video projects **Solution:** * Check for active videos using this connection * Wait for ongoing video rendering to complete * Update video projects to use a different connection * Disable the connection instead of deleting it *** ## Important Considerations **Before Deleting a Connection** 1. **Check for active usage**: Ensure no videos are currently being processed with this connection 2. **Document settings**: Save the AWS Account ID, Region, and IAM role configuration if you may need to recreate it 3. **Update workflows**: Modify any automated workflows that reference this connection ID 4. **Consider disabling**: Use the [Update endpoint](/api-reference/aws-integration/update-aws-connection) to disable instead of delete **Alternative to Deletion** Instead of permanently deleting a connection, consider: * **Disabling it**: Set `enabled: false` using the [Update endpoint](/api-reference/aws-integration/update-aws-connection) * **Renaming it**: Add "DEPRECATED" to the name to mark it as inactive * This preserves the configuration for future reference while preventing its use **When to Delete vs Disable** **Delete when:** * AWS account has been closed * Connection was created for testing only * Security requires removing all traces of the connection **Disable when:** * Temporarily pausing usage * May need to re-enable later * Want to preserve configuration for reference *** ## Deletion Impact When you delete an AWS connection: 1. **Immediate Effects:** * Connection ID becomes invalid immediately * Cannot be used in new video creation requests * Connection appears in no API listings 2. **Video Projects:** * Existing videos remain accessible * New videos cannot be created using this connection * In-progress videos using this connection may fail 3. **Cannot Be Undone:** * Deleted connections cannot be recovered * Must create a new connection to restore access * New connection will have a different connection ID *** ## Next Steps View all connections before deleting Verify connection details before deletion Access your API key from your Pictory dashboard Set up a replacement connection # Get AWS Connection by ID Source: https://docs.pictory.ai/api-reference/aws-integration/get-aws-connection-by-id GET https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} Retrieve details of a specific AWS S3 private connection ## Overview This endpoint retrieves detailed information about a specific AWS S3 private connection using its unique connection ID. Use this to verify connection settings, check status, or retrieve configuration details for a particular connection. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## Use Cases Check if a specific connection exists and is properly configured Retrieve AWS account ID, region, and other configuration details Verify if a connection is currently enabled or disabled Review connection settings for compliance or troubleshooting *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the AWS connection you want to retrieve. This is the `connectionId` value returned when you created the connection or from the list connections endpoint. ``` Example: 20251217080657842fux3au9kh1p0j5s ``` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## Response Returns the AWS connection object with all configuration details including connection ID, AWS account information, and connection status. ### Response Examples ```json 200 - Success theme={null} { "connectionId": "20251217080657842fux3au9kh1p0j5s", "name": "TestAWSConnection", "description": "Test AWS Connection for Documentation", "awsAccountId": "123456789012", "awsRegion": "us-east-2", "type": "AWS", "enabled": true, "createdDate": "2025-12-17T08:06:56.862Z", "updatedDate": "2025-12-17T08:06:56.862Z", "version": 1 } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Connection not found" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` Replace `YOUR_CONNECTION_ID` with the actual connection ID you want to retrieve ```bash cURL theme={null} # Retrieve a specific AWS connection by ID # Replace YOUR_API_KEY with your actual API key # Replace YOUR_CONNECTION_ID with your connection ID curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/awsconnections/YOUR_CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key // Replace 'YOUR_CONNECTION_ID' with your connection ID const getAwsConnectionById = async (apiKey, connectionId) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/awsconnections/${connectionId}`, { method: 'GET', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const connectionId = 'YOUR_CONNECTION_ID'; // ← Replace with your connection ID const result = await getAwsConnectionById('YOUR_API_KEY', connectionId); // ← Replace with your API key console.log('Connection Details:', result); console.log('Connection Name:', result.name); console.log('AWS Region:', result.awsRegion); console.log('Enabled:', result.enabled); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key # Replace 'YOUR_CONNECTION_ID' with your connection ID import requests def get_aws_connection_by_id(api_key, connection_id): """Retrieve a specific AWS S3 connection by ID""" url = f"https://api.pictory.ai/pictoryapis/v1/awsconnections/{connection_id}" headers = { "Authorization": api_key } response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Usage try: connection_id = "YOUR_CONNECTION_ID" # ← Replace with your connection ID result = get_aws_connection_by_id("YOUR_API_KEY", connection_id) # ← Replace with your API key print(f"Connection Details: {result}") print(f"Connection Name: {result['name']}") print(f"AWS Region: {result['awsRegion']}") print(f"Enabled: {result['enabled']}") except requests.exceptions.HTTPError as e: if e.response.status_code == 404: print("Error: Connection not found") else: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key // Replace "YOUR_CONNECTION_ID" with your connection ID package main import ( "encoding/json" "fmt" "io" "net/http" ) type AwsConnection struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` AWSAccountID string `json:"awsAccountId"` AWSRegion string `json:"awsRegion"` Type string `json:"type"` CreatedDate string `json:"createdDate"` UpdatedDate string `json:"updatedDate"` Version int `json:"version"` } func getAwsConnectionById(apiKey, connectionId string) (*AwsConnection, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/awsconnections/%s", connectionId) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("connection not found") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result AwsConnection if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { connectionId := "YOUR_CONNECTION_ID" // ← Replace with your connection ID result, err := getAwsConnectionById("YOUR_API_KEY", connectionId) // ← Replace with your API key if err != nil { panic(err) } fmt.Printf("Connection Details: %+v\n", result) fmt.Printf("Connection Name: %s\n", result.Name) fmt.Printf("AWS Region: %s\n", result.AWSRegion) fmt.Printf("Enabled: %v\n", result.Enabled) } ``` *** ## Common Use Cases ### Verify Connection Before Using in Video Creation Before creating a video with private S3 assets, verify the connection is still active: ```javascript theme={null} // Check if connection exists and is enabled const connection = await getAwsConnectionById(apiKey, connectionId); if (connection.enabled) { console.log('Connection is active and ready to use'); // Proceed with video creation } else { console.log('Connection is disabled. Enable it before creating videos.'); } ``` ### Get Connection Configuration Details ```python theme={null} # Retrieve connection details for troubleshooting connection = get_aws_connection_by_id(api_key, connection_id) print(f"AWS Account ID: {connection['awsAccountId']}") print(f"AWS Region: {connection['awsRegion']}") print(f"Created: {connection['createdDate']}") print(f"Last Updated: {connection['updatedDate']}") ``` ### Check Connection Status ```javascript theme={null} // Check if a specific connection is enabled const connection = await getAwsConnectionById(apiKey, connectionId); const status = connection.enabled ? 'Active' : 'Disabled'; console.log(`Connection "${connection.name}" is ${status}`); ``` *** ## Error Handling **Cause:** The connection ID does not exist or has been deleted **Solution:** * Verify the connection ID is correct * Use the [Get AWS Connections](/api-reference/aws-integration/get-aws-connections) endpoint to list all available connections * Check if the connection was deleted **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** You do not have permission to access this connection **Solution:** * Verify the connection belongs to your account * Check that your API key has the necessary permissions * Contact support if you believe you should have access *** ## Next Steps Retrieve all your AWS S3 private connections Set up a new AWS S3 private connection Access your API key from your Pictory dashboard Learn how to use connection IDs in video creation # Get AWS Private Connections Source: https://docs.pictory.ai/api-reference/aws-integration/get-aws-connections GET https://api.pictory.ai/pictoryapis/v1/awsconnections Retrieve all your configured AWS S3 private connections ## Overview This endpoint retrieves a list of all AWS S3 private connections you have configured for your Pictory account. Use this to view your existing connections, check their status, and get connection IDs for use in video creation requests. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## Use Cases See all your configured AWS S3 connections in one place Retrieve connection IDs to use in video creation requests Verify which connections are currently enabled or disabled Review your AWS account IDs and regions *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/awsconnections ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## Response Returns an array of AWS connection objects in the `Items` field. If no connections are configured, returns an empty array. ### Response Examples ```json 200 - Success (With Connections) theme={null} { "Items": [ { "connectionId": "20251217080657842fux3au9kh1p0j5s", "name": "TestAWSConnection", "description": "Test AWS Connection for Documentation", "awsAccountId": "123456789012", "awsRegion": "us-east-2", "type": "AWS", "enabled": true, "createdDate": "2025-12-17T08:06:56.862Z", "updatedDate": "2025-12-17T08:06:56.862Z", "version": 1 } ] } ``` ```json 200 - Success (No Connections) theme={null} { "Items": [] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Retrieve all AWS connections # Replace YOUR_API_KEY with your actual API key curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/awsconnections \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const getAwsConnections = async (apiKey) => { const response = await fetch('https://api.pictory.ai/pictoryapis/v1/awsconnections', { method: 'GET', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const result = await getAwsConnections('YOUR_API_KEY'); // ← Replace with your API key console.log('Your AWS Connections:', result.Items); // Get the first connection ID if (result.Items.length > 0) { console.log('First Connection ID:', result.Items[0].connectionId); } } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def get_aws_connections(api_key): """Retrieve all AWS S3 connections""" url = "https://api.pictory.ai/pictoryapis/v1/awsconnections" headers = { "Authorization": api_key } response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Usage try: result = get_aws_connections("YOUR_API_KEY") # ← Replace with your API key print(f"Your AWS Connections: {result['Items']}") # Get the first connection ID if len(result['Items']) > 0: print(f"First Connection ID: {result['Items'][0]['connectionId']}") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} 0) { echo "First Connection ID: " . $result['Items'][0]['connectionId'] . "\n"; } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type AwsConnection struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` AWSAccountID string `json:"awsAccountId"` AWSRegion string `json:"awsRegion"` Version int `json:"version"` } type AwsConnectionsResponse struct { Items []AwsConnection `json:"Items"` } func getAwsConnections(apiKey string) (*AwsConnectionsResponse, error) { url := "https://api.pictory.ai/pictoryapis/v1/awsconnections" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result AwsConnectionsResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { result, err := getAwsConnections("YOUR_API_KEY") // ← Replace with your API key if err != nil { panic(err) } fmt.Printf("Your AWS Connections: %+v\n", result.Items) // Get the first connection ID if len(result.Items) > 0 { fmt.Printf("First Connection ID: %s\n", result.Items[0].ConnectionID) } } ``` *** ## Common Use Cases ### Find Connection ID for Video Creation When creating videos with private S3 assets, you need to provide the `awsConnectionId`. Use this endpoint to retrieve it: ```javascript theme={null} // Get all connections and find the one you need const result = await getAwsConnections(apiKey); const myConnection = result.Items.find(conn => conn.name === 'PictoryPrivateVideosConnection'); if (myConnection) { console.log('Use this ID in video requests:', myConnection.connectionId); } ``` ### Check if Connections are Enabled ```python theme={null} result = get_aws_connections(api_key) for connection in result['Items']: status = "Active" if connection['enabled'] else "Disabled" print(f"{connection['name']}: {status}") ``` ### List All Regions ```javascript theme={null} const result = await getAwsConnections(apiKey); const regions = result.Items.map(conn => conn.awsRegion); console.log('Configured regions:', [...new Set(regions)]); ``` *** ## Next Steps Set up a new AWS S3 private connection Learn how to use connection IDs in video creation Access your API key from your Pictory dashboard Common issues and solutions # Update AWS Private Connection Source: https://docs.pictory.ai/api-reference/aws-integration/update-aws-connection PUT https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} Modify an existing AWS S3 private connection configuration ## Overview This endpoint allows you to update an existing AWS S3 private connection. You can modify the connection name, description, or enable/disable the connection without changing the AWS account ID or region. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. **Important:** You cannot change the AWS Account ID or AWS Region of an existing connection. If you need to change these values, create a new connection instead. *** ## Use Cases Update the connection name for better organization Modify the description to reflect usage changes Temporarily disable a connection without deleting it Update connection settings as your needs change *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v1/awsconnections/{connectionid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the AWS connection you want to update. This is the `connectionId` value returned when you created the connection. ``` Example: 20251217080657842fux3au9kh1p0j5s ``` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. Must be set to `application/json` ### Body Parameters **Do NOT include `awsRegion` or `awsAccountId` in the request body.** These fields are immutable and including them will cause errors. Updated name for the AWS connection **Example:** `"UpdatedConnectionName"` Updated description of the AWS connection **Example:** `"Updated connection description"` Whether the connection should be active (`true`) or disabled (`false`). If not provided, the existing value is preserved. **Example:** `true` The current version number of the connection (for optimistic locking). This prevents concurrent updates from overwriting each other. Get the current version from the [Get Connection by ID](/api-reference/aws-integration/get-aws-connection-by-id) endpoint. **Example:** `1` *** ## Response Returns the updated AWS connection object. The `version` number is incremented with each successful update. Note that `awsAccountId`, `awsRegion`, and `connectionId` cannot be changed. ### Response Examples ```json 200 - Success theme={null} { "enabled": true, "name": "UpdatedConnectionName", "description": "Updated connection description", "awsAccountId": "123456789012", "awsRegion": "us-east-2", "connectionId": "20251217080657842fux3au9kh1p0j5s", "version": 2 } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "enabled", "errors": "enabled is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Connection not found" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` Replace `YOUR_CONNECTION_ID` with the actual connection ID you want to update ```bash cURL theme={null} # Update an AWS connection # Replace YOUR_API_KEY with your actual API key # Replace YOUR_CONNECTION_ID with your connection ID curl --request PUT \ --url https://api.pictory.ai/pictoryapis/v1/awsconnections/YOUR_CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "UpdatedConnectionName", "description": "Updated connection description", "enabled": true, "version": 1 }' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key // Replace 'YOUR_CONNECTION_ID' with your connection ID const updateAwsConnection = async (apiKey, connectionId, updates) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/awsconnections/${connectionId}`, { method: 'PUT', headers: { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const connectionId = 'YOUR_CONNECTION_ID'; // ← Replace with your connection ID const updates = { name: 'UpdatedConnectionName', description: 'Updated connection description', enabled: true, version: 1 // Get current version from Get Connection by ID endpoint }; const result = await updateAwsConnection('YOUR_API_KEY', connectionId, updates); // ← Replace with your API key console.log('Updated Connection:', result); console.log('New Version:', result.version); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key # Replace 'YOUR_CONNECTION_ID' with your connection ID import requests def update_aws_connection(api_key, connection_id, updates): """Update an existing AWS S3 connection""" url = f"https://api.pictory.ai/pictoryapis/v1/awsconnections/{connection_id}" headers = { "Authorization": api_key, "Content-Type": "application/json" } response = requests.put(url, headers=headers, json=updates) response.raise_for_status() return response.json() # Usage try: connection_id = "YOUR_CONNECTION_ID" # ← Replace with your connection ID updates = { "name": "UpdatedConnectionName", "description": "Updated connection description", "enabled": True, "version": 1 # Get current version from Get Connection by ID endpoint } result = update_aws_connection("YOUR_API_KEY", connection_id, updates) # ← Replace with your API key print(f"Updated Connection: {result}") print(f"New Version: {result['version']}") except requests.exceptions.HTTPError as e: if e.response.status_code == 404: print("Error: Connection not found") else: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} 'UpdatedConnectionName', 'description' => 'Updated connection description', 'enabled' => true, 'version' => 1 // Get current version from Get Connection by ID endpoint ]; $result = updateAwsConnection('YOUR_API_KEY', $connectionId, $updates); // ← Replace with your API key echo "Updated Connection:\n"; print_r($result); echo "New Version: " . $result['version'] . "\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key // Replace "YOUR_CONNECTION_ID" with your connection ID package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type UpdateAwsConnectionRequest struct { Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` Version int `json:"version"` } type AwsConnection struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` AWSAccountID string `json:"awsAccountId"` AWSRegion string `json:"awsRegion"` Version int `json:"version"` } func updateAwsConnection(apiKey, connectionId string, updates UpdateAwsConnectionRequest) (*AwsConnection, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/awsconnections/%s", connectionId) jsonData, err := json.Marshal(updates) if err != nil { return nil, err } req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode == http.StatusNotFound { return nil, fmt.Errorf("connection not found") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result AwsConnection if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { connectionId := "YOUR_CONNECTION_ID" // ← Replace with your connection ID updates := UpdateAwsConnectionRequest{ Name: "UpdatedConnectionName", Description: "Updated connection description", Enabled: true, Version: 1, // Get current version from Get Connection by ID endpoint } result, err := updateAwsConnection("YOUR_API_KEY", connectionId, updates) // ← Replace with your API key if err != nil { panic(err) } fmt.Printf("Updated Connection: %+v\n", result) fmt.Printf("New Version: %d\n", result.Version) } ``` *** ## Common Use Cases ### Rename a Connection ```javascript theme={null} // Update connection name for better organization // First, get the current connection to retrieve the version const connection = await getAwsConnectionById(apiKey, connectionId); const updates = { name: 'Production-S3-Connection', description: 'Production environment AWS S3 assets', enabled: true, version: connection.version }; const result = await updateAwsConnection(apiKey, connectionId, updates); console.log(`Connection renamed to: ${result.name}`); ``` ### Temporarily Disable a Connection ```python theme={null} # Disable connection without deleting it # First, get the current connection to retrieve the version connection = get_aws_connection_by_id(api_key, connection_id) updates = { "name": "PictoryPrivateVideosConnection", "description": "Pictory Private Videos Connection", "enabled": False, # Disable the connection "version": connection["version"] } result = update_aws_connection(api_key, connection_id, updates) print(f"Connection is now {'enabled' if result['enabled'] else 'disabled'}") ``` ### Re-enable a Disabled Connection ```javascript theme={null} // Re-enable a previously disabled connection // First, get the current connection to retrieve the version const connection = await getAwsConnectionById(apiKey, connectionId); const updates = { name: 'PictoryPrivateVideosConnection', description: 'Pictory Private Videos Connection', enabled: true, // Re-enable the connection version: connection.version }; const result = await updateAwsConnection(apiKey, connectionId, updates); console.log('Connection re-enabled successfully'); ``` ### Update Description Only ```python theme={null} # Update just the description, keep other fields unchanged # First get the current connection details current = get_aws_connection_by_id(api_key, connection_id) # Update with new description updates = { "name": current["name"], # Keep existing name "description": "Updated description with new information", "enabled": current["enabled"], # Keep existing enabled state "version": current["version"] # Include current version for optimistic locking } result = update_aws_connection(api_key, connection_id, updates) print(f"Description updated: {result['description']}") ``` *** ## Error Handling **Cause:** The connection ID does not exist or has been deleted **Solution:** * Verify the connection ID is correct * Use the [Get AWS Connections](/api-reference/aws-integration/get-aws-connections) endpoint to list all available connections * Check if the connection was deleted **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** Required fields are missing from the request body **Solution:** * Ensure both `name` and `version` fields are included * Verify the request body is valid JSON * The `enabled` field is optional - if omitted, the existing value is preserved **Cause:** Field values do not meet validation requirements **Solution:** * `name` must be a non-empty string (required) * `version` must be a positive integer (required) * `enabled` must be a boolean value (`true` or `false`) if provided (optional) * `description` should be a string if provided (optional) * Do NOT include `awsRegion` or `awsAccountId` in the request - these fields cannot be changed **Cause:** The version number you provided does not match the current version (someone else updated the connection) **Solution:** * Get the latest connection details using the [Get Connection by ID](/api-reference/aws-integration/get-aws-connection-by-id) endpoint * Use the current `version` number from that response * Retry your update request with the new version number This is called "optimistic locking" and prevents concurrent updates from overwriting each other. **Cause:** You do not have permission to update this connection **Solution:** * Verify the connection belongs to your account * Check that your API key has the necessary permissions * Contact support if you believe you should have access *** ## Important Notes **Cannot Change AWS Credentials** You cannot update the `awsAccountId` or `awsRegion` of an existing connection. These values are set when the connection is created and cannot be modified. Do NOT include these fields in your update request. **What happens if you try:** * Including `awsRegion` in the request body will cause a `400 Bad Request` error * Including `awsAccountId` in the request body will cause a `400 Bad Request` error * The `connectionId` is in the URL path and cannot be changed **If you need to change AWS Account ID or Region:** 1. Create a new connection with the correct values using [Create AWS Connection](/api-reference/aws-integration/aws-private-connection) 2. Update your video creation workflows to use the new connection ID 3. Delete the old connection using [Delete AWS Connection](/api-reference/aws-integration/delete-aws-connection) **Version Tracking** Each successful update increments the `version` number of the connection. This helps track configuration changes over time. **Disabling vs Deleting** Instead of deleting a connection you may need later, consider temporarily disabling it by setting `enabled: false`. This preserves the configuration while preventing its use in video creation. *** ## Next Steps Retrieve all your AWS S3 private connections Get details of a specific connection before updating Access your API key from your Pictory dashboard Permanently delete an AWS connection # Get Fonts Source: https://docs.pictory.ai/api-reference/branding/get-fonts GET https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts Retrieve all supported text fonts available for video generation in Pictory ## Overview Fetch all supported text fonts available for video generation in the Pictory App. These fonts can be applied to text elements in your video storyboard, including captions, titles, headings, and body text. Use this endpoint to retrieve available fonts for video editing, text customization, or to allow users to select from supported font options when creating or editing video content. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns an array of font names as strings. Each font name represents a supported typeface that can be used in video text elements. List of available font names ### Response Examples ```json 200 - Success theme={null} [ "Arial", "Averia Libre", "Calibri", "Caveat", "Courier Prime", "Dancing Script", "Grandstander", "Helvetica", "Helvetica Neue", "Josefin Sans", "Lato", "Merriweather", "Montserrat", "Moon dance", "Noto Sans", "Open Sans", "Poppins", "ProximaNova", "Quicksand", "Roboto", "Rokkitt", "Rowdies", "Source Sans Pro", "Space mono", "Ubuntu" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() print(f"Total fonts available: {len(fonts)}\n") # Display all fonts for i, font in enumerate(fonts, 1): print(f"{i}. {font}") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const fonts = await response.json(); console.log(`Total fonts available: ${fonts.length}\n`); // Display all fonts fonts.forEach((font, index) => { console.log(`${index + 1}. ${font}`); }); ``` ```php PHP theme={null} $font) { echo ($index + 1) . ". $font\n"; } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) func getFonts(apiKey string) ([]string, error) { url := "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var fonts []string if err := json.Unmarshal(body, &fonts); err != nil { return nil, err } return fonts, nil } // Usage func main() { fonts, err := getFonts("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Total fonts available: %d\n\n", len(fonts)) // Display all fonts for i, font := range fonts { fmt.Printf("%d. %s\n", i+1, font) } } ``` *** ## Common Use Cases ### 1. List All Available Fonts Retrieve and display all supported fonts: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() print(f"Total fonts: {len(fonts)}\n") # Display in columns for i in range(0, len(fonts), 3): row = fonts[i:i+3] print(" ".join(f"{font:<20}" for font in row)) ``` ### 2. Create Font Selector for UI Build a dropdown/selector component: ```python theme={null} import requests def get_fonts_for_selector(api_key): """ Get fonts formatted for a UI selector. Args: api_key: Your API key Returns: List of selector options """ url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() # Format for UI selector selector_options = [ { "value": font, "label": font } for font in sorted(fonts) ] return selector_options # Usage options = get_fonts_for_selector("YOUR_API_KEY") print(f"Font options: {len(options)}\n") # Example output for option in options[:10]: print(f" {option['label']}") ``` ### 3. Categorize Fonts by Type Organize fonts into categories based on their characteristics: ```python theme={null} import requests def categorize_fonts(api_key): """ Categorize fonts based on their characteristics. Args: api_key: Your API key Returns: Dictionary with categorized fonts """ url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() categories = { 'Sans-Serif': [], 'Serif': [], 'Monospace': [], 'Script/Handwriting': [], 'Display/Decorative': [], 'Other': [] } # Categorize fonts based on name patterns sans_serif = ['Arial', 'Helvetica', 'Calibri', 'Lato', 'Open Sans', 'Poppins', 'Roboto', 'Noto Sans', 'Source Sans Pro', 'Josefin Sans', 'Ubuntu', 'ProximaNova', 'Quicksand'] serif = ['Merriweather', 'Rokkitt'] monospace = ['Courier Prime', 'Space mono'] script = ['Dancing Script', 'Caveat', 'Moon dance'] display = ['Grandstander', 'Rowdies', 'Averia Libre', 'Montserrat'] for font in fonts: if font in sans_serif: categories['Sans-Serif'].append(font) elif font in serif: categories['Serif'].append(font) elif font in monospace: categories['Monospace'].append(font) elif font in script: categories['Script/Handwriting'].append(font) elif font in display: categories['Display/Decorative'].append(font) else: categories['Other'].append(font) return categories # Usage categories = categorize_fonts("YOUR_API_KEY") for category, fonts in categories.items(): if fonts: print(f"\n{category} ({len(fonts)} fonts):") for font in fonts: print(f" - {font}") ``` ### 4. Search Fonts by Name Find fonts matching specific criteria: ```python theme={null} import requests def search_fonts(api_key, search_term): """ Search for fonts by name. Args: api_key: Your API key search_term: Text to search for in font names Returns: List of matching font names """ url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() # Case-insensitive search search_term_lower = search_term.lower() matches = [ font for font in fonts if search_term_lower in font.lower() ] return matches # Usage - Find all fonts containing "sans" sans_fonts = search_fonts("YOUR_API_KEY", "sans") print(f"Found {len(sans_fonts)} fonts containing 'sans':") for font in sans_fonts: print(f"- {font}") # Find monospace fonts mono_fonts = search_fonts("YOUR_API_KEY", "mono") print(f"\nFound {len(mono_fonts)} monospace fonts:") for font in mono_fonts: print(f"- {font}") ``` ### 5. Validate Font Names Check if a font name is supported: ```python theme={null} import requests def validate_font(api_key, font_name): """ Validate if a font name is supported. Args: api_key: Your API key font_name: The font name to validate Returns: Tuple of (is_valid, exact_match or None) """ url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) fonts = response.json() # Exact match (case-sensitive) if font_name in fonts: return True, font_name # Case-insensitive match for font in fonts: if font.lower() == font_name.lower(): return True, font return False, None # Usage test_fonts = ["Arial", "arial", "Roboto", "Times New Roman", "Poppins"] for font in test_fonts: is_valid, exact_match = validate_font("YOUR_API_KEY", font) if is_valid: print(f"✓ '{font}' is valid (exact: '{exact_match}')") else: print(f"✗ '{font}' is not supported") ``` *** ## Font Categories The supported fonts can be grouped into the following categories: | Category | Fonts | Use Case | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | **Sans-Serif** | Arial, Helvetica, Calibri, Lato, Open Sans, Poppins, Roboto, Noto Sans, Source Sans Pro, Josefin Sans, Ubuntu, ProximaNova, Quicksand | Modern, clean text; body copy; UI elements | | **Serif** | Merriweather, Rokkitt | Traditional, formal text; headings; elegant designs | | **Monospace** | Courier Prime, Space mono | Code snippets; technical content; typewriter effect | | **Script/Handwriting** | Dancing Script, Caveat, Moon dance | Personal touch; invitations; creative designs | | **Display/Decorative** | Grandstander, Rowdies, Averia Libre, Montserrat | Headlines; attention-grabbing text; branding | *** ## Font Characteristics ### Sans-Serif Fonts **Best for:** Modern designs, body text, readability on screens * **Arial** - Classic, widely supported * **Helvetica / Helvetica Neue** - Professional, clean * **Roboto** - Modern, geometric * **Open Sans** - Friendly, optimized for web * **Lato** - Warm, contemporary * **Poppins** - Geometric, playful * **Montserrat** - Urban, sophisticated ### Serif Fonts **Best for:** Traditional designs, printed materials, elegance * **Merriweather** - Designed for screens, highly readable * **Rokkitt** - Geometric slab serif, distinctive ### Monospace Fonts **Best for:** Code, technical documentation, retro designs * **Courier Prime** - Classic typewriter style * **Space mono** - Quirky, geometric ### Script & Handwriting Fonts **Best for:** Personal touch, creative projects, invitations * **Dancing Script** - Elegant, flowing * **Caveat** - Casual, handwritten * **Moon dance** - Playful, decorative ### Display & Decorative Fonts **Best for:** Headlines, logos, attention-grabbing text * **Grandstander** - Bold, energetic * **Rowdies** - Fun, rounded * **Averia Libre** - Artistic, unique *** ## Usage Notes **Font Names are Case-Sensitive**: When applying fonts to video elements, use the exact font name as returned by this endpoint, including capitalization. **Caching Recommended**: The list of supported fonts changes infrequently. Cache the results locally to improve performance and reduce API calls. **Fallback Fonts**: If a specified font is unavailable, the system will fall back to a default font (typically Arial or Roboto). *** ## Best Practices 1. **Cache Font List**: Fetch fonts once and cache them locally. The list changes infrequently. 2. **Use Exact Names**: Always use the exact font name as returned by the API, including capitalization and spacing. 3. **Provide Font Preview**: In user interfaces, display visual previews of fonts in their actual typeface to help users make selections. 4. **Group by Category**: Organize fonts into logical categories (Sans-Serif, Serif, Script, etc.) for better user experience. 5. **Sort Alphabetically**: Present fonts in alphabetical order for easy browsing. 6. **Validate Font Names**: Before applying a font to a video element, verify the name exists in the current list of supported fonts. 7. **Consider Readability**: Choose fonts appropriate for the video context: * **Captions/Subtitles**: Sans-serif fonts (Arial, Roboto, Open Sans) * **Headlines**: Display or bold sans-serif fonts (Montserrat, Poppins) * **Formal Content**: Serif fonts (Merriweather) * **Creative/Personal**: Script fonts (Dancing Script, Caveat) 8. **Test Font Sizes**: Different fonts have different optical sizes. Test readability at various sizes. 9. **Limit Font Variety**: Use 2-3 fonts maximum per video for visual consistency. 10. **Accessibility**: Ensure sufficient contrast between text and background. Avoid overly decorative fonts for important information. *** ## Integration with Video Storyboard Apply fonts when creating or updating video storyboard elements: ```python theme={null} import requests def apply_font_to_text_element(api_key, project_id, font_name): """ Example of applying a font to a text element in a video storyboard. Args: api_key: Your API key project_id: The project ID font_name: The font to apply Note: This is a conceptual example. The actual implementation depends on the storyboard update endpoint structure. """ # First, validate the font exists fonts_url = "https://api.pictory.ai/pictoryapis/v1/video/storyboard/fonts" headers = { "Authorization": api_key, "accept": "application/json" } fonts_response = requests.get(fonts_url, headers=headers) fonts = fonts_response.json() # Check if font exists if font_name not in fonts: # Try case-insensitive match font_match = next((f for f in fonts if f.lower() == font_name.lower()), None) if font_match: font_name = font_match else: raise ValueError(f"Font '{font_name}' not supported") # Apply font to storyboard element print(f"Applying font '{font_name}' to project {project_id}") # Implementation depends on storyboard update endpoint # Usage apply_font_to_text_element( "YOUR_API_KEY", "project-123", "Poppins" ) ``` *** ## Popular Font Combinations ### Professional & Clean * **Heading:** Montserrat (Bold) * **Body:** Open Sans (Regular) * **Caption:** Roboto (Light) ### Modern & Friendly * **Heading:** Poppins (SemiBold) * **Body:** Lato (Regular) * **Caption:** Lato (Light) ### Classic & Elegant * **Heading:** Merriweather (Bold) * **Body:** Lato (Regular) * **Caption:** Lato (Italic) ### Creative & Playful * **Heading:** Grandstander (Bold) * **Body:** Quicksand (Regular) * **Caption:** Quicksand (Light) ### Technical & Modern * **Heading:** Roboto (Bold) * **Body:** Roboto (Regular) * **Code:** Courier Prime (Regular) *** ## Related Endpoints * [Get Text Styles](get-text-styles) - Retrieve text styling presets * [Get Video Brands](get-video-brands) - Retrieve video brand configurations * [Video Storyboard](video-storyboard) - Create and manage video storyboards # Get Text Styles Source: https://docs.pictory.ai/api-reference/branding/get-text-styles GET https://api.pictory.ai/pictoryapis/v1/styles Retrieve all available text styles defined in the Pictory application ## Overview Fetch all text styles defined in the Pictory App. Text styles are predefined formatting presets that control the appearance of text in your videos, including font, size, color, weight, alignment, and other visual properties. These styles can be applied to captions, titles, headings, and body text in your video projects. Use this endpoint to retrieve available styles for video editing, branding customization, or to allow users to select from predefined text formatting options. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/styles ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns an array of text style objects, each containing a unique identifier and descriptive name. List of available text styles Unique identifier for the text style Descriptive name of the text style (e.g., "Navy blue", "Bold edge", "Standard Black Caption") ### Response Examples ```json 200 - Success theme={null} [ { "id": "8f3deae9-fe38-4c53-8be0-616bef1da916", "name": "Navy blue" }, { "id": "8b715161-182a-4ef4-8130-b04873f464f6", "name": "Indigo ink" }, { "id": "6d9cc665-8087-4587-ab6d-f9d7d01f82c4", "name": "Default" }, { "id": "4111e523-eb9c-41a2-b17b-c312694ce4eb", "name": "Sleek" }, { "id": "fc2737ae-d88b-4914-8b8d-5152d94dc3f6", "name": "Black Traditional Caption" }, { "id": "c8d30e79-a7cc-4ae4-8cf9-2c6286abb407", "name": "Dark Blue Caption" }, { "id": "bcbcef60-9f8f-4357-ac32-ecf5144f2f12", "name": "Headliner" }, { "id": "ac666d90-f402-42b3-8b69-738afa09741c", "name": "Flash" } ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/styles' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) styles = response.json() print(f"Total styles available: {len(styles)}\n") # Display first 10 styles for style in styles[:10]: print(f"- {style['name']} (ID: {style['id']})") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/styles', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const styles = await response.json(); console.log(`Total styles available: ${styles.length}\n`); // Display first 10 styles styles.slice(0, 10).forEach(style => { console.log(`- ${style.name} (ID: ${style.id})`); }); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type TextStyle struct { ID string `json:"id"` Name string `json:"name"` } func getTextStyles(apiKey string) ([]TextStyle, error) { url := "https://api.pictory.ai/pictoryapis/v1/styles" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var styles []TextStyle if err := json.Unmarshal(body, &styles); err != nil { return nil, err } return styles, nil } // Usage func main() { styles, err := getTextStyles("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Total styles available: %d\n\n", len(styles)) // Display first 10 styles limit := 10 if len(styles) < limit { limit = len(styles) } for i := 0; i < limit; i++ { fmt.Printf("- %s (ID: %s)\n", styles[i].Name, styles[i].ID) } } ``` *** ## Common Use Cases ### 1. List All Available Styles Retrieve and display all text styles: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) styles = response.json() print(f"Total styles: {len(styles)}\n") # Group by category (based on name patterns) captions = [s for s in styles if 'caption' in s['name'].lower()] headings = [s for s in styles if 'heading' in s['name'].lower() or 'title' in s['name'].lower()] others = [s for s in styles if s not in captions and s not in headings] print(f"Caption styles: {len(captions)}") print(f"Heading/Title styles: {len(headings)}") print(f"Other styles: {len(others)}") ``` ### 2. Search Styles by Name Find styles matching specific criteria: ```python theme={null} import requests def search_styles(api_key, search_term): """ Search for text styles by name. Args: api_key: Your API key search_term: Text to search for in style names """ url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) styles = response.json() # Case-insensitive search search_term_lower = search_term.lower() matches = [ style for style in styles if search_term_lower in style['name'].lower() ] return matches # Usage - Find all "black" styles black_styles = search_styles("YOUR_API_KEY", "black") print(f"Found {len(black_styles)} styles containing 'black':") for style in black_styles: print(f"- {style['name']}") # Find caption styles caption_styles = search_styles("YOUR_API_KEY", "caption") print(f"\nFound {len(caption_styles)} caption styles") ``` ### 3. Create Style Selector for UI Build a dropdown/selector component: ```python theme={null} import requests def get_styles_for_selector(api_key, category=None): """ Get text styles formatted for a UI selector. Args: api_key: Your API key category: Optional filter ('caption', 'heading', 'title', etc.) """ url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) styles = response.json() # Filter by category if specified if category: styles = [ s for s in styles if category.lower() in s['name'].lower() ] # Format for UI selector selector_options = [ { "value": style['id'], "label": style['name'] } for style in sorted(styles, key=lambda x: x['name']) ] return selector_options # Usage all_options = get_styles_for_selector("YOUR_API_KEY") caption_options = get_styles_for_selector("YOUR_API_KEY", category="caption") print(f"All styles: {len(all_options)}") print(f"Caption styles: {len(caption_options)}") # Example output for first 5 print("\nFirst 5 options:") for option in all_options[:5]: print(f" {option['label']} -> {option['value']}") ``` ### 4. Cache Styles Locally Cache styles to reduce API calls: ```python theme={null} import requests import json from datetime import datetime, timedelta class StyleCache: def __init__(self, api_key, cache_duration_hours=24): self.api_key = api_key self.cache_duration = timedelta(hours=cache_duration_hours) self.cache_file = "text_styles_cache.json" self.styles = None self.last_updated = None def get_styles(self, force_refresh=False): """Get styles from cache or API.""" # Check if cache is valid if not force_refresh and self._is_cache_valid(): print("Using cached styles") return self.styles # Fetch from API print("Fetching styles from API") url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": self.api_key, "accept": "application/json" } response = requests.get(url, headers=headers) self.styles = response.json() self.last_updated = datetime.now() # Save to cache file self._save_cache() return self.styles def _is_cache_valid(self): """Check if cache exists and is still valid.""" try: with open(self.cache_file, 'r') as f: cache_data = json.load(f) last_updated = datetime.fromisoformat(cache_data['last_updated']) if datetime.now() - last_updated < self.cache_duration: self.styles = cache_data['styles'] self.last_updated = last_updated return True except (FileNotFoundError, json.JSONDecodeError, KeyError): pass return False def _save_cache(self): """Save styles to cache file.""" cache_data = { 'styles': self.styles, 'last_updated': self.last_updated.isoformat() } with open(self.cache_file, 'w') as f: json.dump(cache_data, f) # Usage cache = StyleCache("YOUR_API_KEY", cache_duration_hours=24) # First call - fetches from API styles = cache.get_styles() print(f"Loaded {len(styles)} styles") # Second call - uses cache styles = cache.get_styles() print(f"Loaded {len(styles)} styles") # Force refresh styles = cache.get_styles(force_refresh=True) print(f"Loaded {len(styles)} styles (refreshed)") ``` ### 5. Group Styles by Category Organize styles into logical categories: ```python theme={null} import requests from collections import defaultdict def categorize_styles(api_key): """ Categorize text styles based on their names. Args: api_key: Your API key Returns: Dictionary with categorized styles """ url = "https://api.pictory.ai/pictoryapis/v1/styles" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) styles = response.json() categories = { 'Captions': [], 'Headings/Titles': [], 'Colors': [], 'Standard': [], 'Special Effects': [], 'Other': [] } for style in styles: name_lower = style['name'].lower() if 'caption' in name_lower: categories['Captions'].append(style) elif 'heading' in name_lower or 'title' in name_lower: categories['Headings/Titles'].append(style) elif any(color in name_lower for color in ['black', 'blue', 'yellow', 'red', 'purple', 'orange', 'green', 'pink']): categories['Colors'].append(style) elif 'standard' in name_lower or 'default' in name_lower: categories['Standard'].append(style) elif any(effect in name_lower for effect in ['bold', 'flash', 'retro', 'handwritten', 'cartoon']): categories['Special Effects'].append(style) else: categories['Other'].append(style) return categories # Usage categories = categorize_styles("YOUR_API_KEY") for category, styles in categories.items(): print(f"\n{category} ({len(styles)} styles):") for style in styles[:5]: # Show first 5 of each category print(f" - {style['name']}") if len(styles) > 5: print(f" ... and {len(styles) - 5} more") ``` *** ## Style Categories Based on the returned styles, common categories include: | Category | Examples | Use Case | | ------------------- | -------------------------------------------------------------------- | -------------------------------- | | **Captions** | Black Traditional Caption, Dark Blue Caption, Standard White Caption | Subtitles and closed captions | | **Headings** | Bold One-word Heading, Orange Handwritten Heading, Headliner | Video titles and section headers | | **Colors** | Navy blue, Sunflower yellow, Lemon Yellow, Indigo ink | Color-themed text | | **Standard** | Default, Standard, Body text, Sub heading, Heading | General-purpose text | | **Branded** | Sleek, Classic mini, Clean, Bold edge, Polished pro | Consistent branding | | **Special Effects** | Flash, Retro, Handwritten, Typewriter, Cartoon | Creative and stylized text | | **News/Media** | Red Breaking News Caption, Large Yellow News Heading | News and announcements | *** ## Usage Notes **Caching Recommended**: Text styles rarely change. Cache the results locally to improve performance and reduce API calls. **Style IDs**: Use the UUID `id` field when applying styles to video elements. Style names are for display purposes only. **Consistent Across Projects**: The same text styles are available across all your video projects, ensuring brand consistency. *** ## Best Practices 1. **Cache Styles**: Fetch styles once and cache them locally. Text styles change infrequently. 2. **Use Style IDs**: Always reference styles by their UUID `id`, not by name. Names may change, but IDs remain constant. 3. **Provide Search/Filter**: In user interfaces, provide search or filter functionality to help users find specific styles quickly from the large list. 4. **Group by Category**: Organize styles into logical categories (captions, headings, colors) for better user experience. 5. **Show Previews**: If possible, display visual previews of text styles to help users make selections. 6. **Handle Missing Styles Gracefully**: If a referenced style ID does not exist, fall back to a default style. 7. **Sort Alphabetically**: Present styles in alphabetical order for easy browsing. 8. **Validate Style IDs**: Before applying a style, verify the ID exists in the current list of available styles. # Get Video Brands Source: https://docs.pictory.ai/api-reference/branding/get-video-brands GET https://api.pictory.ai/pictoryapis/v1/brands/video Retrieve all video brand configurations defined in the Pictory application ## Overview Fetch all video brand configurations defined in the Pictory App. Video brands are customizable presets that define your video's visual identity, including color schemes, fonts, logos, animations, and other branding elements. These brands ensure consistent styling across all your video projects. Use this endpoint to retrieve available brand configurations for video editing, maintaining brand consistency, or allowing users to select from predefined branding options for their video projects. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/brands/video ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns an array of video brand objects, each containing a unique identifier and descriptive name. List of available video brands Unique identifier for the video brand Descriptive name of the video brand (e.g., "Default Brand", "Corporate Blue", "Modern Minimalist") ### Response Examples ```json 200 - Success theme={null} [ { "id": "eb67b4ba-0eef-41fa-a772-d9675cc3645c", "name": "Default Brand" }, { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Corporate Blue" }, { "id": "f8e7d6c5-b4a3-9281-7065-43210fedcba9", "name": "Modern Minimalist" }, { "id": "12345678-90ab-cdef-1234-567890abcdef", "name": "Bold & Vibrant" } ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/brands/video' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) brands = response.json() print(f"Total video brands available: {len(brands)}\n") # Display all brands for brand in brands: print(f"- {brand['name']} (ID: {brand['id']})") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/brands/video', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const brands = await response.json(); console.log(`Total video brands available: ${brands.length}\n`); // Display all brands brands.forEach(brand => { console.log(`- ${brand.name} (ID: ${brand.id})`); }); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type VideoBrand struct { ID string `json:"id"` Name string `json:"name"` } func getVideoBrands(apiKey string) ([]VideoBrand, error) { url := "https://api.pictory.ai/pictoryapis/v1/brands/video" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var brands []VideoBrand if err := json.Unmarshal(body, &brands); err != nil { return nil, err } return brands, nil } // Usage func main() { brands, err := getVideoBrands("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Total video brands available: %d\n\n", len(brands)) // Display all brands for _, brand := range brands { fmt.Printf("- %s (ID: %s)\n", brand.Name, brand.ID) } } ``` *** ## Common Use Cases ### 1. List All Available Video Brands Retrieve and display all video brands: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) brands = response.json() print(f"Total brands: {len(brands)}\n") # Display with details for i, brand in enumerate(brands, 1): print(f"{i}. {brand['name']}") print(f" ID: {brand['id']}") print() ``` ### 2. Create Brand Selector for UI Build a dropdown/selector component: ```python theme={null} import requests def get_brands_for_selector(api_key): """ Get video brands formatted for a UI selector. Args: api_key: Your API key Returns: List of selector options """ url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) brands = response.json() # Format for UI selector selector_options = [ { "value": brand['id'], "label": brand['name'] } for brand in sorted(brands, key=lambda x: x['name']) ] return selector_options # Usage options = get_brands_for_selector("YOUR_API_KEY") print(f"Brand options: {len(options)}\n") # Example output for option in options: print(f" {option['label']} -> {option['value']}") ``` ### 3. Search Brands by Name Find brands matching specific criteria: ```python theme={null} import requests def search_brands(api_key, search_term): """ Search for video brands by name. Args: api_key: Your API key search_term: Text to search for in brand names Returns: List of matching brand objects """ url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) brands = response.json() # Case-insensitive search search_term_lower = search_term.lower() matches = [ brand for brand in brands if search_term_lower in brand['name'].lower() ] return matches # Usage - Find all "default" brands default_brands = search_brands("YOUR_API_KEY", "default") print(f"Found {len(default_brands)} brands containing 'default':") for brand in default_brands: print(f"- {brand['name']}") ``` ### 4. Cache Brands Locally Cache brands to reduce API calls: ```python theme={null} import requests import json from datetime import datetime, timedelta class BrandCache: def __init__(self, api_key, cache_duration_hours=24): self.api_key = api_key self.cache_duration = timedelta(hours=cache_duration_hours) self.cache_file = "video_brands_cache.json" self.brands = None self.last_updated = None def get_brands(self, force_refresh=False): """Get brands from cache or API.""" # Check if cache is valid if not force_refresh and self._is_cache_valid(): print("Using cached brands") return self.brands # Fetch from API print("Fetching brands from API") url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": self.api_key, "accept": "application/json" } response = requests.get(url, headers=headers) self.brands = response.json() self.last_updated = datetime.now() # Save to cache file self._save_cache() return self.brands def _is_cache_valid(self): """Check if cache exists and is still valid.""" try: with open(self.cache_file, 'r') as f: cache_data = json.load(f) last_updated = datetime.fromisoformat(cache_data['last_updated']) if datetime.now() - last_updated < self.cache_duration: self.brands = cache_data['brands'] self.last_updated = last_updated return True except (FileNotFoundError, json.JSONDecodeError, KeyError): pass return False def _save_cache(self): """Save brands to cache file.""" cache_data = { 'brands': self.brands, 'last_updated': self.last_updated.isoformat() } with open(self.cache_file, 'w') as f: json.dump(cache_data, f) # Usage cache = BrandCache("YOUR_API_KEY", cache_duration_hours=24) # First call - fetches from API brands = cache.get_brands() print(f"Loaded {len(brands)} brands") # Second call - uses cache brands = cache.get_brands() print(f"Loaded {len(brands)} brands") # Force refresh brands = cache.get_brands(force_refresh=True) print(f"Loaded {len(brands)} brands (refreshed)") ``` ### 5. Validate Brand ID Before Use Check if a brand ID exists: ```python theme={null} import requests def validate_brand_id(api_key, brand_id): """ Validate if a brand ID exists in available brands. Args: api_key: Your API key brand_id: The brand ID to validate Returns: Tuple of (is_valid, brand_name or None) """ url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) brands = response.json() # Search for brand by ID for brand in brands: if brand['id'] == brand_id: return True, brand['name'] return False, None # Usage brand_id = "eb67b4ba-0eef-41fa-a772-d9675cc3645c" is_valid, brand_name = validate_brand_id("YOUR_API_KEY", brand_id) if is_valid: print(f"✓ Brand ID is valid: {brand_name}") else: print("✗ Brand ID not found") ``` *** ## Brand Configuration Video brands typically include the following elements: | Element | Description | Purpose | | ----------------- | ------------------------------------- | ------------------------------------- | | **Color Palette** | Primary, secondary, and accent colors | Consistent color scheme across videos | | **Typography** | Font families, sizes, and weights | Text styling and readability | | **Logo** | Company or personal logo | Brand identity and recognition | | **Intro/Outro** | Opening and closing sequences | Professional video bookends | | **Lower Thirds** | Text overlays for names and titles | Professional presentation | | **Transitions** | Animation styles between scenes | Smooth visual flow | | **Music Theme** | Default background music style | Audio branding | | **Watermark** | Brand watermark positioning | Content ownership | *** ## Usage Notes **Caching Recommended**: Video brands rarely change. Cache the results locally to improve performance and reduce API calls. **Brand IDs**: Use the UUID `id` field when applying brands to video projects. Brand names are for display purposes only. **Consistent Branding**: The same video brands are available across all your video projects, ensuring brand consistency. *** ## Best Practices 1. **Cache Brands**: Fetch brands once and cache them locally. Video brands change infrequently. 2. **Use Brand IDs**: Always reference brands by their UUID `id`, not by name. Names may change, but IDs remain constant. 3. **Default Brand**: Most accounts have a "Default Brand" that can be used as a fallback option. 4. **Validate Brand IDs**: Before applying a brand to a project, verify the ID exists in the current list of available brands. 5. **Sort Alphabetically**: Present brands in alphabetical order for easy browsing in user interfaces. 6. **Handle Missing Brands Gracefully**: If a referenced brand ID does not exist, fall back to the default brand. 7. **Brand Previews**: If possible, display visual previews of brand styles to help users make selections. 8. **User Permissions**: Ensure users have appropriate permissions to access and use specific brands. *** ## Integration with Projects Apply a video brand to a project during creation or updates: ```python theme={null} import requests def apply_brand_to_project(api_key, project_id, brand_id): """ Apply a video brand to an existing project. Args: api_key: Your API key project_id: The project to update brand_id: The brand ID to apply """ # First, validate the brand exists brands_url = "https://api.pictory.ai/pictoryapis/v1/brands/video" headers = { "Authorization": api_key, "accept": "application/json" } brands_response = requests.get(brands_url, headers=headers) brands = brands_response.json() # Check if brand exists brand_exists = any(b['id'] == brand_id for b in brands) if not brand_exists: raise ValueError(f"Brand ID {brand_id} not found") # Apply brand to project (example - actual endpoint may vary) print(f"Applying brand {brand_id} to project {project_id}") # Implementation depends on project update endpoint # Usage apply_brand_to_project( "YOUR_API_KEY", "project-123", "eb67b4ba-0eef-41fa-a772-d9675cc3645c" ) ``` *** ## Related Endpoints * [Get Text Styles](get-text-styles) - Retrieve text styling options * [Get Projects](get-projects) - List all video projects * [Update Project](update-project) - Modify project settings including brand # Clean Job Source: https://docs.pictory.ai/api-reference/jobs/clean-job DELETE https://api.pictory.ai/pictoryapis/v1/jobs/{jobid}/clean Remove job data and associated resources for a completed job ## Overview Remove all job data and associated resources for a specific completed job. This operation permanently deletes all files generated by the job including video outputs, audio files, thumbnails, subtitle files (SRT, VTT, TXT), share URLs, preview URLs, and any uploaded input files. Use this endpoint to free up storage space and remove sensitive data after you have downloaded or processed the job results. This operation is only available for jobs that have completed (successfully or failed) and is **irreversible**. **Irreversible Operation**: Once a job is cleaned, all associated data is permanently deleted and cannot be recovered. Make sure you have downloaded any needed outputs before cleaning. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} DELETE https://api.pictory.ai/pictoryapis/v1/jobs/{jobid}/clean ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the job to clean up. This is returned when the job was initially created. **Example:** `"17684c46-9d14-44ed-8830-ff839713ef8b"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns a simple success indicator confirming the job and all associated resources have been cleaned. Indicates whether the cleanup operation was successful. True if the job was successfully cleaned up, false otherwise. ### Response Examples ```json 200 - Success theme={null} { "success": true } ``` ```json 400 - Job Still Processing theme={null} { "error": { "code": "INVALID_STATE", "message": "Cannot clean job that is still in progress" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Job Not Found theme={null} { "error": { "code": "NOT_FOUND", "message": "The specified job ID does not exist" } } ``` ```json 400 - Bad Request theme={null} { "error": { "code": "INVALID_REQUEST", "message": "The request was malformed" } } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred while processing the request" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and `YOUR_JOB_ID` with the job ID you want to clean ```bash cURL theme={null} curl --request DELETE \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/17684c46-9d14-44ed-8830-ff839713ef8b/clean' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests job_id = "17684c46-9d14-44ed-8830-ff839713ef8b" url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}/clean" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.delete(url, headers=headers) data = response.json() if data.get("success"): print(f"Job {job_id} successfully cleaned") else: print(f"Failed to clean job {job_id}") ``` ```javascript JavaScript / Node.js theme={null} const jobId = '17684c46-9d14-44ed-8830-ff839713ef8b'; const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}/clean`; const response = await fetch(url, { method: 'DELETE', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } }); const data = await response.json(); if (data.success) { console.log(`Job ${jobId} successfully cleaned`); } else { console.log(`Failed to clean job ${jobId}`); } ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" and "YOUR_JOB_ID" with actual values package main import ( "encoding/json" "fmt" "io" "net/http" ) type CleanResponse struct { Success bool `json:"success"` } func cleanJob(apiKey, jobID string) (*CleanResponse, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s/clean", jobID) req, err := http.NewRequest("DELETE", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result CleanResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { jobID := "17684c46-9d14-44ed-8830-ff839713ef8b" result, err := cleanJob("YOUR_API_KEY", jobID) if err != nil { panic(err) } if result.Success { fmt.Printf("Job %s successfully cleaned\n", jobID) } else { fmt.Printf("Failed to clean job %s\n", jobID) } } ``` *** ## Usage Notes **Data Loss**: This operation permanently deletes all job data and outputs. Ensure you have downloaded any needed files before cleaning the job. **Job State Requirement**: Jobs can only be cleaned after they have completed (either successfully or failed). You cannot clean jobs that are still processing. **Storage Management**: Regularly clean completed jobs to manage storage usage and costs, especially for high-volume applications. **Idempotent Operation**: Calling this endpoint multiple times with the same job ID is safe. Subsequent calls will return success even if the job was already cleaned. *** ## Common Use Cases ### 1. Clean Job After Downloading Results Download results and then clean up: ```python theme={null} import requests import time def download_and_clean_job(api_key, job_id, output_dir="."): """ Download job results and then clean up the job. Args: api_key: Your API key job_id: The job ID to process output_dir: Directory to save downloaded files """ base_url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } # Step 1: Get job status and results get_url = f"{base_url}/{job_id}" response = requests.get(get_url, headers=headers) data = response.json() if not data.get("success"): print(f"Job {job_id} not successful") return False status = data.get("data", {}).get("status") if status != "completed": print(f"Job not completed yet. Status: {status}") return False # Step 2: Download outputs (example for video render job) output_url = data.get("data", {}).get("outputUrl") if output_url: print(f"Downloading output from {output_url}") output_response = requests.get(output_url) output_file = f"{output_dir}/job_{job_id}_output.mp4" with open(output_file, 'wb') as f: f.write(output_response.content) print(f"Saved to {output_file}") # Step 3: Save transcript if available transcript = data.get("data", {}).get("transcript") if transcript: transcript_file = f"{output_dir}/job_{job_id}_transcript.json" with open(transcript_file, 'w') as f: import json json.dump(transcript, f, indent=2) print(f"Saved transcript to {transcript_file}") # Step 4: Clean up the job clean_url = f"{base_url}/{job_id}/clean" clean_response = requests.delete(clean_url, headers=headers) clean_data = clean_response.json() if clean_data.get("success"): print(f"Job {job_id} successfully cleaned") return True else: print(f"Failed to clean job {job_id}") return False # Usage download_and_clean_job("YOUR_API_KEY", "17684c46-9d14-44ed-8830-ff839713ef8b") ``` ### 2. Batch Clean Multiple Completed Jobs Clean multiple jobs at once: ```python theme={null} import requests from concurrent.futures import ThreadPoolExecutor, as_completed def clean_single_job(api_key, job_id): """Clean a single job.""" url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}/clean" headers = { "Authorization": api_key, "accept": "application/json" } try: response = requests.delete(url, headers=headers) data = response.json() return { "job_id": job_id, "success": data.get("success", False), "error": data.get("error") if not data.get("success") else None } except Exception as e: return { "job_id": job_id, "success": False, "error": str(e) } def batch_clean_jobs(api_key, job_ids, max_workers=5): """ Clean multiple jobs in parallel. Args: api_key: Your API key job_ids: List of job IDs to clean max_workers: Maximum number of parallel requests """ results = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_job = { executor.submit(clean_single_job, api_key, job_id): job_id for job_id in job_ids } for future in as_completed(future_to_job): result = future.result() results.append(result) if result["success"]: print(f"✓ Cleaned job {result['job_id']}") else: print(f"✗ Failed to clean job {result['job_id']}: {result.get('error')}") # Summary successful = sum(1 for r in results if r["success"]) failed = len(results) - successful print(f"\nSummary: {successful} cleaned, {failed} failed") return results # Usage job_ids_to_clean = [ "17684c46-9d14-44ed-8830-ff839713ef8b", "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0", "another-job-id-here" ] batch_clean_jobs("YOUR_API_KEY", job_ids_to_clean) ``` ### 3. Clean Old Completed Jobs Automatically clean jobs older than a certain age: ```python theme={null} import requests from datetime import datetime, timedelta def clean_old_jobs(api_key, days_old=7): """ Clean jobs that completed more than X days ago. Args: api_key: Your API key days_old: Clean jobs older than this many days """ base_url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } # Step 1: Get all jobs response = requests.get(base_url, headers=headers) data = response.json() if not data.get("items"): print("No jobs found") return cutoff_date = datetime.now() - timedelta(days=days_old) jobs_to_clean = [] # Step 2: Filter old completed jobs for job in data["items"]: job_data = job.get("data", {}) status = job_data.get("status") # Only clean completed or failed jobs if status not in ["completed", "failed"]: continue # Check if job has timestamp (you may need to track this separately) # For demonstration, we'll clean all completed jobs # In production, you'd check the completion timestamp jobs_to_clean.append(job_data.get("jobId")) print(f"Found {len(jobs_to_clean)} jobs to clean") # Step 3: Clean the jobs cleaned_count = 0 for job_id in jobs_to_clean: if not job_id: continue clean_url = f"{base_url}/{job_id}/clean" clean_response = requests.delete(clean_url, headers=headers) clean_data = clean_response.json() if clean_data.get("success"): cleaned_count += 1 print(f"Cleaned job {job_id}") print(f"\nCleaned {cleaned_count} out of {len(jobs_to_clean)} jobs") # Usage clean_old_jobs("YOUR_API_KEY", days_old=7) ``` ### 4. Clean with Confirmation Require user confirmation before cleaning: ```python theme={null} import requests def clean_job_with_confirmation(api_key, job_id): """ Get job details and ask for confirmation before cleaning. Args: api_key: Your API key job_id: The job ID to clean """ base_url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } # Step 1: Get job information get_url = f"{base_url}/{job_id}" response = requests.get(get_url, headers=headers) data = response.json() if not data.get("success"): print(f"Failed to retrieve job {job_id}") return False job_data = data.get("data", {}) status = job_data.get("status", "unknown") # Step 2: Display job information print(f"\nJob ID: {job_id}") print(f"Status: {status}") if "transcript" in job_data: print(f"Has Transcript: Yes ({len(job_data['transcript'])} segments)") if "highlight" in job_data: print(f"Has Highlights: Yes") if "outputUrl" in job_data: print(f"Output URL: {job_data['outputUrl']}") # Step 3: Ask for confirmation print("\n⚠️ WARNING: This will permanently delete all job data and outputs!") confirm = input("Are you sure you want to clean this job? (yes/no): ") if confirm.lower() != "yes": print("Cleanup cancelled") return False # Step 4: Clean the job clean_url = f"{base_url}/{job_id}/clean" clean_response = requests.delete(clean_url, headers=headers) clean_data = clean_response.json() if clean_data.get("success"): print(f"✓ Job {job_id} successfully cleaned") return True else: print(f"✗ Failed to clean job {job_id}") return False # Usage clean_job_with_confirmation("YOUR_API_KEY", "17684c46-9d14-44ed-8830-ff839713ef8b") ``` ### 5. Conditional Cleanup Based on Job Type Clean jobs based on type and retention policy: ```python theme={null} import requests def smart_clean_jobs(api_key, retention_policies=None): """ Clean jobs based on type-specific retention policies. Args: api_key: Your API key retention_policies: Dict mapping job types to whether they should be cleaned """ if retention_policies is None: retention_policies = { "transcription": True, # Clean transcription jobs "highlight": True, # Clean highlight jobs "render": False, # Keep render jobs "template": False # Keep template jobs } base_url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } # Get all jobs response = requests.get(base_url, headers=headers) data = response.json() cleaned = 0 skipped = 0 for job in data.get("items", []): job_data = job.get("data", {}) job_id = job_data.get("jobId") status = job_data.get("status") if not job_id or status not in ["completed", "failed"]: continue # Determine job type job_type = None if "transcript" in job_data: job_type = "transcription" elif "highlight" in job_data: job_type = "highlight" elif "outputUrl" in job_data: job_type = "render" # Check retention policy should_clean = retention_policies.get(job_type, False) if should_clean: clean_url = f"{base_url}/{job_id}/clean" clean_response = requests.delete(clean_url, headers=headers) if clean_response.json().get("success"): print(f"✓ Cleaned {job_type} job {job_id}") cleaned += 1 else: print(f"→ Kept {job_type} job {job_id}") skipped += 1 print(f"\nCleaned: {cleaned}, Kept: {skipped}") # Usage smart_clean_jobs("YOUR_API_KEY") ``` *** ## What Gets Deleted When you clean a job, the following data and resources are permanently removed: | Resource Type | Description | | ------------------- | ----------------------------------------------------------- | | **Video Outputs** | All rendered video files in various formats and resolutions | | **Audio Files** | Generated audio tracks, voiceovers, and audio exports | | **Thumbnails** | Preview images and video thumbnails | | **Subtitle Files** | SRT, VTT, and TXT subtitle/caption files | | **Transcript Data** | Word-level transcript with timing information | | **Highlight Data** | AI-generated highlight segments and summaries | | **Share URLs** | Public sharing links and preview URLs | | **Preview URLs** | Temporary preview and playback URLs | | **Input Files** | Uploaded source files (videos, audio, images) | | **Project Data** | Intermediate processing files and temporary data | *** ## Best Practices 1. **Download Before Cleaning**: Always download and backup any needed outputs before cleaning a job. 2. **Verify Job Completion**: Ensure the job has completed (successfully or failed) before attempting to clean. Processing jobs cannot be cleaned. 3. **Archive Important Results**: For jobs with important results, save the full job data (transcript, highlights, outputs) to your own storage before cleaning. 4. **Automate Cleanup**: Implement automated cleanup policies to regularly clean old jobs and manage storage costs. 5. **Handle Errors Gracefully**: Jobs may already be cleaned or deleted. Handle 404 errors appropriately. 6. **Use Batch Operations**: When cleaning multiple jobs, use parallel requests with reasonable rate limiting. 7. **Implement Retention Policies**: Define clear policies for how long different types of jobs should be retained. 8. **Log Cleanup Operations**: Maintain logs of cleaned jobs for audit and recovery purposes. 9. **Confirm Critical Operations**: For interactive applications, require user confirmation before cleaning jobs. 10. **Check Storage Limits**: Monitor your account's storage usage and clean jobs proactively to avoid reaching limits. *** ## Error Handling Common errors and how to handle them: | Error | Cause | Solution | | ---------------- | ------------------------------------- | ----------------------------------------- | | `INVALID_STATE` | Job is still processing | Wait for job to complete before cleaning | | `NOT_FOUND` | Job does not exist or already cleaned | This is safe to ignore in cleanup scripts | | `UNAUTHORIZED` | Invalid or expired API key | Verify your API key is valid | | `INTERNAL_ERROR` | Server-side error | Retry with exponential backoff | # List Jobs Source: https://docs.pictory.ai/api-reference/jobs/get-jobs GET https://api.pictory.ai/pictoryapis/v1/jobs Retrieve a paginated list of all processing jobs for your API client ## Overview Retrieve a paginated list of all jobs associated with your API client. This endpoint returns jobs in descending order by creation date (newest first), allowing you to monitor all asynchronous processing tasks such as video transcriptions, summaries, renders, and template creations. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters Base64-encoded pagination token returned from a previous request. Use this to retrieve the next page of results. If not provided, the first page of results will be returned. **Example:** `"eyJsYXN0RXZhbHVhdGVkS2V5Ijp7fX0="` *** ## Response Returns a paginated list of job objects in the `items` array. Each job includes its unique `job_id`, current status, and job-specific data including transcripts, highlights, render results, or error information. If more results are available, a `nextPageKey` is included for pagination. Array of job objects, ordered by creation date in descending order (newest first) Indicates whether the job completed successfully. True if the job is in-progress or completed successfully, false if it failed or encountered an error. Contains the job-specific data including status, progress, results, or error information. The exact structure depends on the job type and current state. Array of transcript segments with speaker identification and word-level timing (for transcription jobs) Array of highlight segments extracted from the transcript (for summary/highlight jobs) Current status of the job (e.g., "processing", "completed", "failed") The unique identifier (UUID) of the job Error information if the job failed Error message describing what went wrong Error code for programmatic handling Base64-encoded pagination token to retrieve the next page of results. If null, there are no more pages available. Pass this value as the `nextPageKey` query parameter in the next request to fetch additional results. ### Response Examples ```json 200 - Success theme={null} { "items": [ { "success": true, "data": { "transcript": [ { "uid": "63773537-fb4c-4f93-8bed-217951b16fb2", "speakerId": 1, "words": [ { "uid": "49297a70-5f5d-459c-8486-618f7bc983d3", "word": "Once", "start_time": 0, "end_time": 0.48, "speakerId": 1, "sentence_index": 0 }, { "uid": "0cd65dd6-1c37-4b1c-8e9e-2772541dc21a", "word": "again,", "start_time": 0.48, "end_time": 0.88, "speakerId": 1, "sentence_index": 0 } ] } ], "jobId": "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0", "status": "completed" } } ], "nextPageKey": "eyJsYXN0RXZhbHVhdGVkS2V5Ijp7ImNyZWF0ZWRfYXQiOiIyMDI1LTAxLTAxVDEyOjAwOjAwWiJ9fQ==" } ``` ```json 200 - Empty Results theme={null} { "items": [], "nextPageKey": null } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 400 - Bad Request theme={null} { "error": { "code": "INVALID_REQUEST", "message": "The request was malformed" } } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred while processing the request" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() # Display job information for job in data.get("items", []): print(f"Job Status: {'Success' if job['success'] else 'Failed'}") if 'jobId' in job.get('data', {}): print(f" Job ID: {job['data']['jobId']}") if 'status' in job.get('data', {}): print(f" Status: {job['data']['status']}") print("---") # Check for more pages if data.get("nextPageKey"): print(f"More results available. Next page key: {data['nextPageKey']}") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/jobs', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const data = await response.json(); // Display job information data.items?.forEach(job => { console.log(`Job Status: ${job.success ? 'Success' : 'Failed'}`); if (job.data?.jobId) { console.log(` Job ID: ${job.data.jobId}`); } if (job.data?.status) { console.log(` Status: ${job.data.status}`); } console.log('---'); }); // Check for more pages if (data.nextPageKey) { console.log(`More results available. Next page key: ${data.nextPageKey}`); } ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" ) type JobData struct { JobID string `json:"jobId,omitempty"` Status string `json:"status,omitempty"` Transcript []map[string]interface{} `json:"transcript,omitempty"` Highlight []map[string]interface{} `json:"highlight,omitempty"` Error *ErrorInfo `json:"error,omitempty"` } type ErrorInfo struct { Message string `json:"message"` Code string `json:"code"` } type Job struct { Success bool `json:"success"` Data JobData `json:"data"` } type JobsResponse struct { Items []Job `json:"items"` NextPageKey *string `json:"nextPageKey"` } func getJobs(apiKey string, nextPageKey *string) (*JobsResponse, error) { baseURL := "https://api.pictory.ai/pictoryapis/v1/jobs" if nextPageKey != nil && *nextPageKey != "" { params := url.Values{} params.Add("nextPageKey", *nextPageKey) baseURL += "?" + params.Encode() } req, err := http.NewRequest("GET", baseURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result JobsResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { data, err := getJobs("YOUR_API_KEY", nil) if err != nil { panic(err) } fmt.Printf("Total jobs on this page: %d\n\n", len(data.Items)) // Display job information for _, job := range data.Items { status := "Success" if !job.Success { status = "Failed" } fmt.Printf("Job Status: %s\n", status) if job.Data.JobID != "" { fmt.Printf(" Job ID: %s\n", job.Data.JobID) } if job.Data.Status != "" { fmt.Printf(" Status: %s\n", job.Data.Status) } fmt.Println("---") } // Check for more pages if data.NextPageKey != nil && *data.NextPageKey != "" { fmt.Printf("More results available. Next page key: %s\n", *data.NextPageKey) } } ``` *** ## Usage Notes Jobs are returned in reverse chronological order by creation date, with the most recently created jobs appearing first. **Pagination**: When you have many jobs, use the `nextPageKey` value from the response to retrieve subsequent pages. Pass the `nextPageKey` as a query parameter in your next request. **Job Types**: Jobs can represent various async operations including video transcriptions, summaries/highlights, project renders, and template creations. The `data` structure varies based on the job type. **Rate Limiting**: Be mindful of API rate limits when polling for job status. Consider implementing exponential backoff or using webhooks for job completion notifications. *** ## Common Use Cases ### 1. List All Recent Jobs Retrieve and display all recent jobs: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() print(f"Total jobs: {len(data['items'])}\n") for job in data['items']: print(f"Job Status: {'Success' if job['success'] else 'Failed'}") if 'jobId' in job.get('data', {}): print(f" Job ID: {job['data']['jobId']}") if 'status' in job.get('data', {}): print(f" Status: {job['data']['status']}") # Check for errors if not job['success'] and 'error' in job.get('data', {}): print(f" Error: {job['data']['error'].get('message', 'Unknown error')}") print("---") ``` ### 2. Paginate Through All Jobs Fetch all jobs across multiple pages: ```python theme={null} import requests def fetch_all_jobs(api_key): url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } all_jobs = [] next_page_key = None while True: # Add pagination parameter if we have a next page key params = {"nextPageKey": next_page_key} if next_page_key else {} response = requests.get(url, headers=headers, params=params) data = response.json() # Add jobs from this page all_jobs.extend(data.get('items', [])) # Check if there are more pages next_page_key = data.get('nextPageKey') if not next_page_key: break return all_jobs # Usage all_jobs = fetch_all_jobs("YOUR_API_KEY") print(f"Total jobs across all pages: {len(all_jobs)}") # Count by status completed = sum(1 for job in all_jobs if job['success']) failed = len(all_jobs) - completed print(f"Completed: {completed}, Failed: {failed}") ``` ### 3. Monitor Job Status Check the status of specific job types: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() # Filter transcription jobs transcription_jobs = [ job for job in data['items'] if 'transcript' in job.get('data', {}) ] print(f"Transcription jobs found: {len(transcription_jobs)}") for job in transcription_jobs: job_id = job.get('data', {}).get('jobId', 'Unknown') status = job.get('data', {}).get('status', 'Unknown') success = job.get('success', False) print(f"Job ID: {job_id}") print(f" Status: {status}") print(f" Success: {success}") if 'transcript' in job.get('data', {}): transcript_segments = len(job['data']['transcript']) print(f" Transcript Segments: {transcript_segments}") print("---") ``` ### 4. Find Failed Jobs Identify and log failed jobs for debugging: ```python theme={null} import requests from datetime import datetime url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() # Find failed jobs failed_jobs = [job for job in data['items'] if not job['success']] if failed_jobs: print(f"Found {len(failed_jobs)} failed jobs:\n") for job in failed_jobs: job_id = job.get('data', {}).get('jobId', 'Unknown') error_msg = job.get('data', {}).get('error', {}).get('message', 'No error message') error_code = job.get('data', {}).get('error', {}).get('code', 'No error code') print(f"Job ID: {job_id}") print(f" Error Code: {error_code}") print(f" Error Message: {error_msg}") print("---") else: print("No failed jobs found!") ``` ### 5. Export Jobs to CSV Export job data for reporting: ```python theme={null} import requests import csv def export_jobs_to_csv(api_key, filename="jobs_export.csv"): url = "https://api.pictory.ai/pictoryapis/v1/jobs" headers = { "Authorization": api_key, "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() # Prepare CSV with open(filename, 'w', newline='') as csvfile: fieldnames = ['Job ID', 'Status', 'Success', 'Has Transcript', 'Has Highlight', 'Error'] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for job in data.get('items', []): job_data = job.get('data', {}) row = { 'Job ID': job_data.get('jobId', 'N/A'), 'Status': job_data.get('status', 'N/A'), 'Success': 'Yes' if job.get('success') else 'No', 'Has Transcript': 'Yes' if 'transcript' in job_data else 'No', 'Has Highlight': 'Yes' if 'highlight' in job_data else 'No', 'Error': job_data.get('error', {}).get('message', 'None') } writer.writerow(row) print(f"Exported {len(data.get('items', []))} jobs to {filename}") # Usage export_jobs_to_csv("YOUR_API_KEY") ``` *** ## Job Data Structure The `data` field in each job object varies based on the job type: ### Transcription Jobs ```json theme={null} { "jobId": "uuid", "status": "completed", "transcript": [ { "uid": "segment-uuid", "speakerId": 1, "words": [ { "uid": "word-uuid", "word": "Hello", "start_time": 0.0, "end_time": 0.5, "speakerId": 1, "sentence_index": 0 } ] } ] } ``` ### Summary/Highlight Jobs ```json theme={null} { "jobId": "uuid", "status": "completed", "highlight": [ { "start": 0.0, "end": 10.5, "text": "Summary segment text", "importance": 0.95 } ] } ``` ### Failed Jobs ```json theme={null} { "jobId": "uuid", "status": "failed", "error": { "code": "PROCESSING_ERROR", "message": "Failed to process media file" } } ``` # Get Storyboard Preview Job by ID Source: https://docs.pictory.ai/api-reference/jobs/get-storyboard-preview-job-by-id GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} Retrieve the status and results of a storyboard preview job ## Overview This endpoint retrieves the current status and results of a storyboard preview job using its unique job ID. While processing is in progress, it returns the current job status. Once the job completes, it returns the full storyboard preview data, including render parameters, the processed storyboard, and the preview URL. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the storyboard preview job. This value is the `jobId` returned by the [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) endpoint. **Example:** `"a1d36612-326d-4b81-aece-411f8aed4c70"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response ### In-Progress Response Returned while the storyboard preview is still being generated: The unique identifier of the storyboard preview job `true` while the job is processing `"in-progress"` — the storyboard preview is still being generated ### Failed Response Returned when the storyboard preview job has failed during processing: The unique identifier of the storyboard preview job `false` when the job has failed `"failed"` — the storyboard preview generation has failed Error code identifying the failure type (e.g., `"TEXT_TO_VIDEO_FAILED"`) Descriptive message explaining the cause of the failure ### Completed Response Returned when the storyboard preview has been successfully generated. The response contains the full preview data: The unique identifier of the storyboard preview job `true` when the job has completed successfully Contains the complete storyboard preview results. `"completed"` — the storyboard preview has been generated successfully The rendering data used for previewing and rendering videos. This object contains the complete scene composition, including output settings, audio elements, background visuals, and text overlays. Video output configuration Video width in pixels (e.g., `1920`) Video height in pixels (e.g., `1080`) Output format (e.g., `"mp4"`) Output file name Video title Array of video elements including audio tracks (voice-over, background music), background visuals (videos, images, solid colors), and text overlays (scene text, titles). Each element includes `startTime`, `duration`, `elementType`, and type-specific properties. Array of scene marker objects that define scene boundaries. Each marker contains: * `time` (number) — Scene duration in seconds * `showSceneNumber` (string) — Scene label (e.g., `"Scene 1"`) * `uuid` (string) — Unique scene identifier * `startTime` (number) — Scene start time in seconds Array of subtitle objects with timing information. Each subtitle contains: * `subtitle` (string) — The subtitle text * `start` (number) — Start time in seconds * `end` (number) — End time in seconds The project ID (matches the job ID) The processed input storyboard. This is the fully resolved version of the original storyboard request, with all scenes broken down, voice-over URLs generated, and visual selections applied. Name of the video project The webhook URL configured for the job Custom data passed through from the original request The smart layout applied to the video Voice-over configuration Background music configuration, including `enabled`, `volume`, `autoMusic`, and `musicUrl` Array of processed scene objects. Each scene contains: * `story` (string) — Scene text with keyword highlights in `` tags * `voiceOver` (object) — Voice-over configuration with the generated audio URL and timing clips * `backgroundMusic` (object) — Per-scene music settings * `background` (object) — Visual background with `visualUrl`, `type`, and settings Language code (e.g., `"en"`) URL to view the storyboard preview in a browser. This URL can also be embedded in an iframe for app integrations. Refer to the [Embed Preview Player](/integrations/storyboard-video-preview/embed-preview-player) guide for details. URL to open the saved project in the [Pictory web app](https://app.pictory.ai). Present only when `saveProject` was set to `true` in the storyboard request. Unique identifier of the saved project. Use this value with the [Render Project](/api-reference/videos/render-project) or [Get Project by ID](/api-reference/projects/get-project-by-id) APIs. Present only when `saveProject` was set to `true` in the storyboard request. Total AI credits consumed for generating AI visuals (images and video clips) in this storyboard. Present only when the storyboard includes scenes with `aiVisual` configuration. The value is the sum of credits used across all AI-generated scenes. ### Response Examples ```json 200 - In Progress theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "in-progress" } } ``` ```json 200 - Completed (abbreviated) theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "renderParams": { "output": { "width": 1920, "height": 1080, "format": "mp4", "name": "demo_text_to_video.mp4", "title": "demo_text_to_video", "description": "", "frameRendererVersion": "v3" }, "elements": [ { "type": "audio", "id": "voiceOver", "elementType": "audioElement", "url": "https://audios-prod.pictorycontent.com/polly/production/projects/.../voiceover.mp3", "segments": [ { "startTime": 0, "duration": 7.5, "volume": 1, "audioTime": 0 } ], "startTime": 0, "duration": 100000 }, { "type": "audio", "id": "bgMusic", "elementType": "audioElement", "fade": true, "url": "https://tracks.melod.ie/track_versions/.../music.mp3", "segments": [ { "startTime": 0, "duration": 7.5, "volume": 0.1, "audioTime": 0 } ], "startTime": 0, "duration": 100000 }, { "id": "backgroundElement_...", "elementType": "backgroundElement", "type": "video", "url": "https://media.gettyimages.com/id/.../video.mp4", "backgroundColor": "rgba(0, 0, 0, 1)", "width": "100%", "objectMode": "cover", "loop": true, "mute": true, "startTime": 0, "duration": 7.5, "visualType": "video", "library": "getty" }, { "fontFamily": "Space Grotesk", "fontSize": "48", "fontColor": "rgb(255,255,255)", "textAlign": "center", "startTime": 0, "duration": 7.5, "id": "...", "elementType": "layerItem", "type": "text", "text": "AI's Impact on Educators and Creators", "width": "90.00%", "preset": "center-center" } ], "sceneMarkers": [ { "time": 7.5, "showSceneNumber": "Scene 1", "uuid": "202603261544455263c22b912026443bea93884eecfd4fd0c", "startTime": 0 }, { "time": 10.5, "showSceneNumber": "Scene 2", "uuid": "202603261544465266e15765260d944fb917c28bda80a01aa", "startTime": 7.5 } ], "subtitles": [ { "subtitle": "AI is poised to significantly impact educators and course creators on social media.", "start": 0, "end": 7.5 }, { "subtitle": "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency.", "start": 7.5, "end": 18 } ], "projectId": "a1d36612-326d-4b81-aece-411f8aed4c70", "projectAuthorId": "Google_113965628153287895479" }, "storyboard": { "videoName": "demo_text_to_video", "webhook": "https://webhook.site/your-webhook-id", "webhookInput": { "aiVoice": "Brian" }, "smartLayoutName": "Wanderlust", "voiceOver": { "enabled": true }, "backgroundMusic": { "enabled": true, "volume": 0.1, "autoMusic": false, "musicUrl": "https://tracks.melod.ie/track_versions/.../music.mp3" }, "scenes": [ { "story": "AI is poised to significantly impact educators and course creators on social media.", "voiceOver": { "enabled": true, "externalVoice": { "voiceUrl": "https://audios-prod.pictorycontent.com/polly/production/projects/.../voiceover.mp3", "clips": [ { "start": 0, "end": 7.535 } ], "amplificationLevel": 0 } }, "backgroundMusic": { "enabled": true }, "background": { "visualUrl": "https://media.gettyimages.com/id/.../video.mp4", "type": "video", "settings": { "mute": true, "loop": true } } } ], "language": "en" }, "previewUrl": "https://video.pictory.ai/v2/preview/a1d36612-326d-4b81-aece-411f8aed4c70?mode=player", "projectUrl": "https://app.pictory.ai/story/20260326192215799c0a92885a9db478594c3840411873b6a", "projectId": "20260326192215799c0a92885a9db478594c3840411873b6a", "aiCreditsUsed": 24 } } ``` The `projectUrl` and `projectId` fields are present only when `saveProject` was set to `true` in the original [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) request. ```json 200 - Failed theme={null} { "job_id": "0bb0116b-d476-4b60-85ac-ce40ed711991", "success": false, "data": { "status": "failed", "error_code": "TEXT_TO_VIDEO_FAILED", "error_message": "The AI voice speaker [Timm] is invalid and is not supported. Please provide valid AI voice speaker." } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 200 - Invalid Job ID theme={null} { "id": "a1d36612-326d-4b81-aece-411f8aed4c71", "success": false, "data": { "error_code": "5000", "error_message": "JOB_NOT_FOUND" } } ``` *** ## Key Response Fields | Field | Description | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `renderParams` | The rendering data for the video. To customize the video, modify this data and submit it to the [Render Video](/api-reference/videos/render-video) API. To update the preview with changes, save the modified data using the [Update Storyboard Elements](/api-reference/video-storyboard/update-storyboard-elements) API. After saving, the preview URL will reflect the updates. | | `storyboard` | The processed input storyboard with all scenes resolved. Use this object to submit a new storyboard request with modifications. Re-submissions process faster because the scenes, voices, and visuals have already been resolved. | | `previewUrl` | URL to view the storyboard preview. Open in a browser or embed in an iframe. Refer to the [Embed Preview Player](/integrations/storyboard-video-preview/embed-preview-player) guide for integration details. | | `projectUrl` | URL to open the saved project in the [Pictory web app](https://app.pictory.ai). Present only when `saveProject: true`. | | `projectId` | Unique identifier of the saved project. Use with the [Render Project](/api-reference/videos/render-project) or [Get Project by ID](/api-reference/projects/get-project-by-id) APIs. Present only when `saveProject: true`. | | `aiCreditsUsed` | Total AI credits consumed for generating AI visuals (images and video clips). Present only when the storyboard includes scenes with `aiVisual` configuration. | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and use the `jobId` returned from the [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) endpoint. ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/a1d36612-326d-4b81-aece-411f8aed4c70' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests import time def poll_storyboard_job(api_key, job_id, max_wait=600, poll_interval=15): """ Poll for storyboard preview job completion. Args: api_key: Your Pictory API key job_id: The storyboard preview job ID max_wait: Maximum wait time in seconds (default 10 minutes) poll_interval: Polling interval in seconds (default 15 seconds) """ url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}" headers = { "Authorization": api_key, "accept": "application/json" } start_time = time.time() while time.time() - start_time < max_wait: response = requests.get(url, headers=headers) data = response.json() if not data.get("success"): print("Job failed") return data status = data.get("data", {}).get("status") if status == "in-progress": print(f"Storyboard generation in progress... (elapsed: {int(time.time() - start_time)}s)") time.sleep(poll_interval) continue if status == "completed": job_data = data["data"] print("Storyboard preview ready!") print(f"Preview URL: {job_data.get('previewUrl')}") print(f"Scenes: {len(job_data.get('storyboard', {}).get('scenes', []))}") return data # Unknown status print(f"Unexpected status: {status}") return data print("Timeout waiting for storyboard preview") return None # Usage result = poll_storyboard_job("YOUR_API_KEY", "a1d36612-326d-4b81-aece-411f8aed4c70") if result and result.get("success"): data = result["data"] # Access the preview URL preview_url = data.get("previewUrl") print(f"Preview: {preview_url}") # Access renderParams for customization render_params = data.get("renderParams") # Access processed storyboard for re-submission storyboard = data.get("storyboard") ``` ```javascript JavaScript theme={null} const jobId = 'a1d36612-326d-4b81-aece-411f8aed4c70'; async function pollStoryboardJob(apiKey, jobId, maxWait = 600000, pollInterval = 15000) { const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`; const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); if (!data.success) { console.log('Job failed'); return data; } const status = data.data?.status; if (status === 'in-progress') { console.log(`Storyboard generation in progress... (${Math.round((Date.now() - startTime) / 1000)}s)`); await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } if (status === 'completed') { console.log('Storyboard preview ready!'); console.log(`Preview URL: ${data.data.previewUrl}`); console.log(`Scenes: ${data.data.storyboard?.scenes?.length}`); return data; } console.log(`Unexpected status: ${status}`); return data; } console.log('Timeout waiting for storyboard preview'); return null; } // Usage const result = await pollStoryboardJob('YOUR_API_KEY', jobId); if (result?.success) { const { previewUrl, renderParams, storyboard } = result.data; console.log(`Preview: ${previewUrl}`); } ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) func pollStoryboardJob(apiKey, jobID string, maxWait, pollInterval time.Duration) (map[string]interface{}, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) startTime := time.Now() for time.Since(startTime) < maxWait { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) success, _ := data["success"].(bool) if !success { fmt.Println("Job failed") return data, nil } jobData, _ := data["data"].(map[string]interface{}) status, _ := jobData["status"].(string) if status == "in-progress" { elapsed := int(time.Since(startTime).Seconds()) fmt.Printf("Storyboard generation in progress... (%ds)\n", elapsed) time.Sleep(pollInterval) continue } if status == "completed" { fmt.Println("Storyboard preview ready!") if previewUrl, ok := jobData["previewUrl"].(string); ok { fmt.Printf("Preview URL: %s\n", previewUrl) } return data, nil } fmt.Printf("Unexpected status: %s\n", status) return data, nil } return nil, fmt.Errorf("timeout waiting for storyboard preview") } func main() { result, err := pollStoryboardJob( "YOUR_API_KEY", "a1d36612-326d-4b81-aece-411f8aed4c70", 10*time.Minute, 15*time.Second, ) if err != nil { fmt.Println("Error:", err) return } if success, ok := result["success"].(bool); ok && success { data := result["data"].(map[string]interface{}) if previewUrl, ok := data["previewUrl"].(string); ok { fmt.Printf("Preview: %s\n", previewUrl) } } } ``` *** ## Working with the Completed Response ### Customizing and Rendering the Video After the storyboard preview is ready, the following options are available: Open the `previewUrl` in a browser to review the storyboard preview, including all scenes, visuals, and text overlays. To modify scene visuals, text, or other elements, update the `renderParams.elements` data and save it using the [Update Storyboard Elements](/api-reference/video-storyboard/update-storyboard-elements) API. The preview URL will reflect the saved changes. Submit the `renderParams` data (modified or as-is) to the [Render Video](/api-reference/videos/render-video) API to produce the final rendered video file. ### Re-submitting a Modified Storyboard To change the original storyboard structure (for example, different text, a different voice, or different scenes), use the `storyboard` object from the completed response as the request body for a new [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) request. This approach processes faster because the scenes, voice-over audio, and visual selections have already been resolved. *** ## Polling Best Practices Use a polling interval of **10–30 seconds** when checking job status. Polling too frequently may result in rate limiting. 1. **Use webhooks when possible.** Configure a `webhook` URL in the storyboard request to receive automatic notification when the job completes, rather than polling. 2. **Implement timeouts.** Set a maximum wait time. Storyboard previews typically complete within a few minutes, depending on the number of scenes and voice-over generation requirements. 3. **Cache results.** Once a storyboard job completes, store the `renderParams` and `storyboard` data locally. Completed job data may be cleaned after a retention period. # Get Transcription Job by ID Source: https://docs.pictory.ai/api-reference/jobs/get-transcription-job-by-id GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} Retrieve the status and results of a video transcription job ## Overview This endpoint retrieves the current status and results of a transcription job using its unique job ID. While processing is in progress, it returns the current job status. Once the job completes, it returns the full transcript data including word-level timing, speaker identification, and subtitle formats (SRT, VTT). A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the transcription job. This value is the `jobId` returned by the [Video Transcription](/api-reference/transcription/video-transcription) endpoint. **Example:** `"cbbc5305-3c1c-46f0-bdde-468e5ecd763f"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response ### In-Progress Response Returned while the transcription is still being processed: The unique identifier of the transcription job `true` while the job is processing `"in-progress"` — transcription is still being processed ### Completed Response Returned when the transcription has finished successfully. The response contains the full transcript data: `true` when the job has completed successfully The unique identifier of the transcription job Contains the complete transcription results. Array of transcript segments with speaker identification and word-level timing. Each segment contains: Unique identifier for the transcript segment Speaker identifier for this segment Array of word objects with precise timing information Unique identifier for the word The spoken word text. An empty string indicates a pause marker. Start time in seconds End time in seconds Speaker identifier Index of the sentence this word belongs to `true` if this entry represents a pause rather than a spoken word Size of the pause (e.g., `"small"`). Present only when `is_pause` is `true`. State of the word (e.g., `"active"`). Present only for pause markers. URL of the original media file that was transcribed Metadata about the source media file Duration of the media file in seconds Video width in pixels Video height in pixels Aspect ratio label (e.g., `"sixteen-nine"`) Numeric aspect ratio value (e.g., `1.7777777777777777`) Full transcript as plain text Transcript in SRT subtitle format Transcript in WebVTT subtitle format Language code of the transcription (e.g., `"en-US"`) ### Response Examples ```json 200 - In Progress theme={null} { "job_id": "cbbc5305-3c1c-46f0-bdde-468e5ecd763f", "success": true, "data": { "status": "in-progress" } } ``` ```json 200 - Completed theme={null} { "success": true, "data": { "transcript": [ { "uid": "1c01b891-9cfe-452d-8eb9-d1c82f58b44f", "speakerId": 1, "words": [ { "uid": "21247197-328d-4fa7-9e97-b8f661093438", "word": "", "start_time": 0, "end_time": 0.64, "sentence_index": 0, "is_pause": true, "pause_size": "small", "state": "active", "speakerId": 1 }, { "uid": "031b6091-08c5-436d-ad92-7da64ebbb9c5", "word": "Click", "start_time": 0.64, "end_time": 0.96, "speakerId": 1, "sentence_index": 0 }, { "uid": "6fdf6f9e-8b50-4e32-b09b-9d59a8e4d18c", "word": "the", "start_time": 0.96, "end_time": 1.12, "speakerId": 1, "sentence_index": 0 }, { "uid": "bfad854f-9a5e-4dd3-b6cf-afba0f8a0c9e", "word": "play", "start_time": 1.12, "end_time": 1.36, "speakerId": 1, "sentence_index": 0 }, { "uid": "1e56a014-74f0-4753-bfd5-1a6c6f91d40b", "word": "button", "start_time": 1.36, "end_time": 1.68, "speakerId": 1, "sentence_index": 0 }, { "uid": "c8ec17f0-d3cc-4445-9b5d-83de96cc3d28", "word": "to", "start_time": 1.68, "end_time": 1.92, "speakerId": 1, "sentence_index": 0 }, { "uid": "b6d56fc4-32e1-4121-9254-09db58f4861b", "word": "watch", "start_time": 1.92, "end_time": 2.159, "speakerId": 1, "sentence_index": 0 }, { "uid": "4f55e95c-e337-422c-9df0-753dbda7071e", "word": "the", "start_time": 2.159, "end_time": 2.399, "speakerId": 1, "sentence_index": 0 }, { "uid": "804a4882-ac3f-4972-814b-69c29b0eaab6", "word": "tutorial.", "start_time": 2.399, "end_time": 2.899, "speakerId": 1, "sentence_index": 0 } ] }, { "uid": "bdd8ec04-abeb-4c23-8187-b8a16239b4e3", "speakerId": 1, "words": [ { "uid": "eaf3f3f9-a90d-42e2-9e47-c75719b58480", "word": "It's", "start_time": 5.04, "end_time": 5.359, "speakerId": 1, "sentence_index": 2 }, { "uid": "3f859d6e-39d2-4dfc-94d6-0dc99711e988", "word": "Jimmy", "start_time": 5.359, "end_time": 5.68, "speakerId": 1, "sentence_index": 2 }, { "uid": "7eae2d17-f3ff-4f09-aa7f-71969c6f0bc4", "word": "from", "start_time": 5.68, "end_time": 5.92, "speakerId": 1, "sentence_index": 2 }, { "uid": "b0dbd4b8-2aaa-4a87-9cc9-11385f2199f6", "word": "Victory,", "start_time": 5.92, "end_time": 6.399, "speakerId": 1, "sentence_index": 2 } ] } ], "url": "https://pictory-api-prod.s3.us-east-2.amazonaws.com/2sc7gb6gnnobemlp4jf6u3fdh8/e778154d-56c5-4e00-a2ec-0c06fced116a.mp4", "mediaInfo": { "duration": 451.16, "width": 1920, "aspect_ratio": "sixteen-nine", "aspect_ratio_value": 1.7777777777777777, "height": 1080 }, "txt": "Click the play button to watch the tutorial. Hey. It's Jimmy from Victory, and this is a demo of the AI video editor tool...", "srt": "1\n00:00:00,640 --> 00:00:02,899\nClick the play button to watch the tutorial.\n\n2\n00:00:04,639 --> 00:00:06,879\nHey. It's Jimmy from Victory, and this is\n\n3\n00:00:06,879 --> 00:00:09,939\na demo of the AI video editor tool.\n...", "vtt": "WEBVTT\n\n1\n00:00:00.640 --> 00:00:02.899\n- Click the play button to watch the tutorial.\n\n2\n00:00:04.639 --> 00:00:06.879\n- Hey. It's Jimmy from Victory, and this is\n...", "language": "en-US" }, "job_id": "bb94a94d-32b2-46d0-a541-7021d648f884" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 200 - Invalid Job ID theme={null} { "id": "cbbc5305-3c1c-46f0-bdde-468e5ecd763f", "success": false, "data": { "error_code": "5000", "error_message": "JOB_NOT_FOUND" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and use the `jobId` returned from the [Video Transcription](/api-reference/transcription/video-transcription) endpoint. ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/cbbc5305-3c1c-46f0-bdde-468e5ecd763f' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests import time def poll_transcription_job(api_key, job_id, max_wait=600, poll_interval=15): """ Poll for transcription job completion. Args: api_key: Your Pictory API key job_id: The transcription job ID max_wait: Maximum wait time in seconds (default 10 minutes) poll_interval: Polling interval in seconds (default 15 seconds) """ url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}" headers = { "Authorization": api_key, "accept": "application/json" } start_time = time.time() while time.time() - start_time < max_wait: response = requests.get(url, headers=headers) data = response.json() if not data.get("success"): print("Job failed") return data status = data.get("data", {}).get("status") if status == "in-progress": print(f"Transcription in progress... (elapsed: {int(time.time() - start_time)}s)") time.sleep(poll_interval) continue # Job completed - transcript data is available transcript_data = data.get("data", {}) print(f"Transcription complete!") print(f"Language: {transcript_data.get('language')}") print(f"Duration: {transcript_data.get('mediaInfo', {}).get('duration')}s") print(f"Segments: {len(transcript_data.get('transcript', []))}") return data print("Timeout waiting for transcription") return None # Usage result = poll_transcription_job("YOUR_API_KEY", "cbbc5305-3c1c-46f0-bdde-468e5ecd763f") if result and result.get("success"): data = result["data"] # Access the plain text transcript print(f"\nTranscript:\n{data.get('txt', '')[:500]}...") # Access SRT subtitles srt = data.get("srt", "") print(f"\nSRT preview:\n{srt[:300]}...") ``` ```javascript JavaScript theme={null} const jobId = 'cbbc5305-3c1c-46f0-bdde-468e5ecd763f'; async function pollTranscriptionJob(apiKey, jobId, maxWait = 600000, pollInterval = 15000) { const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`; const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); if (!data.success) { console.log('Job failed'); return data; } const status = data.data?.status; if (status === 'in-progress') { console.log(`Transcription in progress... (${Math.round((Date.now() - startTime) / 1000)}s)`); await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } // Job completed console.log('Transcription complete!'); console.log(`Language: ${data.data.language}`); console.log(`Duration: ${data.data.mediaInfo?.duration}s`); console.log(`Segments: ${data.data.transcript?.length}`); return data; } console.log('Timeout waiting for transcription'); return null; } // Usage const result = await pollTranscriptionJob('YOUR_API_KEY', jobId); if (result?.success) { console.log(`\nTranscript: ${result.data.txt?.substring(0, 500)}...`); } ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) func pollTranscriptionJob(apiKey, jobID string, maxWait, pollInterval time.Duration) (map[string]interface{}, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) startTime := time.Now() for time.Since(startTime) < maxWait { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) success, _ := data["success"].(bool) if !success { fmt.Println("Job failed") return data, nil } jobData, _ := data["data"].(map[string]interface{}) status, _ := jobData["status"].(string) if status == "in-progress" { elapsed := int(time.Since(startTime).Seconds()) fmt.Printf("Transcription in progress... (%ds)\n", elapsed) time.Sleep(pollInterval) continue } // Job completed fmt.Println("Transcription complete!") return data, nil } return nil, fmt.Errorf("timeout waiting for transcription") } func main() { result, err := pollTranscriptionJob( "YOUR_API_KEY", "cbbc5305-3c1c-46f0-bdde-468e5ecd763f", 10*time.Minute, 15*time.Second, ) if err != nil { fmt.Println("Error:", err) return } if success, ok := result["success"].(bool); ok && success { data := result["data"].(map[string]interface{}) if txt, ok := data["txt"].(string); ok { if len(txt) > 500 { txt = txt[:500] + "..." } fmt.Printf("\nTranscript: %s\n", txt) } } } ``` *** ## Understanding the Transcript Response The completed transcription response includes several output formats: | Field | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transcript` | Structured array of segments with word-level timing, speaker IDs, and pause markers. Suitable for building custom subtitle renderers or transcript editors. | | `txt` | Full transcript as plain text. Suitable for search indexing, summarization, or display. | | `srt` | Transcript in SRT subtitle format. Can be saved directly as a `.srt` file for video players. | | `vtt` | Transcript in WebVTT format. Can be saved as a `.vtt` file for web-based video players. | | `mediaInfo` | Source media metadata including duration, dimensions, and aspect ratio. | ### Pause Markers The `transcript` array includes pause markers alongside spoken words. These entries are identified by the following characteristics: * `is_pause` is set to `true` * The `word` field contains an empty string * `pause_size` indicates the duration category (e.g., `"small"`) Pause markers are useful for understanding speech pacing and can be leveraged when building transcript-based editing workflows. *** ## Polling Best Practices Use a polling interval of **10–30 seconds** when checking job status. Polling too frequently may result in rate limiting. 1. **Use webhooks when possible.** Configure a `webhook` URL in the transcription request to receive automatic notification when the job completes, rather than polling. 2. **Implement timeouts.** Set a maximum wait time based on the expected file duration. Longer files require more processing time. 3. **Handle all states.** Verify both the `in-progress` status and the presence of transcript data in the response to determine completion. 4. **Cache results.** Once a transcription job completes, store the results locally. Completed job data may be cleaned after a retention period. # Get Video Render Job by ID Source: https://docs.pictory.ai/api-reference/jobs/get-video-render-job-by-id GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} Retrieve the status and results of a video render job ## Overview This endpoint retrieves the current status and results of a video render job using its unique job ID. While rendering is in progress, it returns the job status along with render progress percentage and state. Once the render completes, it returns the full set of output URLs for the rendered video, audio, subtitles, and thumbnail. A valid API key is required to use this endpoint. Obtain your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/jobs/{jobid} ``` *** ## Request Parameters ### Path Parameters The unique identifier (UUID) of the video render job. This value is the `jobId` returned by the [Render from Preview](/api-reference/videos/render-from-preview), [Render Video](/api-reference/videos/render-video), [Render Storyboard Video](/api-reference/videos/render-storyboard-video), or [Render Project](/api-reference/videos/render-project) endpoints. **Example:** `"265a7c1a-4985-4058-9208-68114f131a2b"` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response ### In-Progress Response Returned while the video is being rendered. The response includes progress information once rendering has started: The unique identifier of the render job `true` while the job is processing `"in-progress"` — the video is still being rendered Rendering progress as a percentage (0–100). Present only after rendering has started. Descriptive progress message (e.g., `"Generating video"`). Present only after rendering has started. Current render state (e.g., `"RUNNING"`). Present only after rendering has started. ### Failed Response Returned when the render job has failed: The unique identifier of the render job `false` when the job has failed `"failed"` — the video render has failed Error code identifying the failure type Descriptive message explaining the cause of the failure ### Completed Response Returned when the render has finished successfully. The response contains all output URLs: The unique identifier of the render job `true` when the job has completed successfully Contains the complete render output. `"completed"` — the video has been rendered successfully `100` — rendering is complete Direct download URL for the rendered video file (MP4) Shareable URL for the rendered video Embeddable URL for the rendered video, suitable for iframe integration Direct download URL for the audio track (MP3) URL for the video thumbnail image (JPG) URL for the SRT subtitle file URL for the plain text transcript file URL for the WebVTT subtitle file Total duration of the rendered video in seconds Time taken to encode and render the video in seconds Total AI credits consumed for generating AI visuals (images and video clips) during rendering. Present only when the video includes scenes with `aiVisual` configuration. The value is the sum of credits used across all AI-generated scenes. ### Response Examples ```json 200 - In Progress (Job Started) theme={null} { "job_id": "265a7c1a-4985-4058-9208-68114f131a2b", "success": true, "data": { "status": "in-progress" } } ``` ```json 200 - In Progress (Rendering) theme={null} { "job_id": "265a7c1a-4985-4058-9208-68114f131a2b", "success": true, "data": { "status": "in-progress", "renderProgress": 2, "renderProgressMessage": "Generating video", "renderState": "RUNNING" } } ``` ```json 200 - Completed theme={null} { "job_id": "265a7c1a-4985-4058-9208-68114f131a2b", "success": true, "data": { "status": "completed", "progress": 100, "videoURL": "https://d3uryq9bhgb5qr.cloudfront.net/.../demo_text_to_video3.mp4", "videoShareURL": "https://video.pictory.ai/.../20260326170050790Tkrsq44GvHibYw5", "videoEmbedURL": "https://video.pictory.ai/embed/.../20260326170050790Tkrsq44GvHibYw5", "audioURL": "https://d3uryq9bhgb5qr.cloudfront.net/.../demo_text_to_video3.mp3", "thumbnail": "https://d3uryq9bhgb5qr.cloudfront.net/.../265a7c1a-4985-4058-9208-68114f131a2b.jpg", "srtFile": "https://d3uryq9bhgb5qr.cloudfront.net/.../6392d927-1ccd-4ea0-a167-a6ad7b643132.srt", "txtFile": "https://d3uryq9bhgb5qr.cloudfront.net/.../438e574f-c9cb-4eed-82ea-8cf0b6629664.txt", "vttFile": "https://d3uryq9bhgb5qr.cloudfront.net/.../a6cc12f5-f016-481f-ab0c-44cba00e048e.vtt", "videoDuration": 65.6, "encodingDuration": 117, "aiCreditsUsed": 48 } } ``` ```json 200 - Failed theme={null} { "job_id": "265a7c1a-4985-4058-9208-68114f131a2b", "success": false, "data": { "status": "failed", "error_code": "TEXT_TO_VIDEO_FAILED", "error_message": "The AI voice speaker [Timm] is invalid and is not supported. Please provide valid AI voice speaker." } } ``` ```json 200 - Invalid Job ID theme={null} { "id": "265a7c1a-4985-4058-9208-68114f131a2b", "success": false, "data": { "error_code": "5000", "error_message": "JOB_NOT_FOUND" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Completed Response Fields | Field | Description | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `videoURL` | Direct download link for the rendered MP4 video file | | `videoShareURL` | Shareable link to view the video on Pictory | | `videoEmbedURL` | Embeddable URL suitable for iframe integration | | `audioURL` | Direct download link for the extracted MP3 audio track | | `thumbnail` | URL for the auto-generated video thumbnail (JPG) | | `srtFile` | SRT subtitle file compatible with standard video players | | `txtFile` | Plain text transcript file | | `vttFile` | WebVTT subtitle file for web-based video players | | `videoDuration` | Total video length in seconds | | `encodingDuration` | Total rendering time in seconds | | `aiCreditsUsed` | Total AI credits consumed for generating AI visuals (images and video clips). Present only when the video includes scenes with `aiVisual` configuration. | *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and use the `jobId` returned from any render endpoint. ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/265a7c1a-4985-4058-9208-68114f131a2b' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests import time def poll_render_job(api_key, job_id, max_wait=900, poll_interval=15): """ Poll for video render job completion. Args: api_key: Your Pictory API key job_id: The render job ID max_wait: Maximum wait time in seconds (default 15 minutes) poll_interval: Polling interval in seconds (default 15 seconds) """ url = f"https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}" headers = { "Authorization": api_key, "accept": "application/json" } start_time = time.time() while time.time() - start_time < max_wait: response = requests.get(url, headers=headers) data = response.json() if not data.get("success"): error_data = data.get("data", {}) print(f"Job failed: {error_data.get('error_message', 'Unknown error')}") return data status = data.get("data", {}).get("status") if status == "in-progress": progress = data["data"].get("renderProgress", 0) message = data["data"].get("renderProgressMessage", "Processing") print(f"Rendering... {progress}% - {message}") time.sleep(poll_interval) continue if status == "completed": result = data["data"] print(f"Render complete!") print(f"Video URL: {result.get('videoURL')}") print(f"Duration: {result.get('videoDuration')}s") print(f"Encoding time: {result.get('encodingDuration')}s") return data if status == "failed": error_msg = data["data"].get("error_message", "Unknown error") print(f"Render failed: {error_msg}") return data print(f"Unexpected status: {status}") return data print("Timeout waiting for render") return None # Usage result = poll_render_job("YOUR_API_KEY", "265a7c1a-4985-4058-9208-68114f131a2b") if result and result.get("success") and result["data"].get("status") == "completed": data = result["data"] print(f"\nVideo: {data['videoURL']}") print(f"Share: {data['videoShareURL']}") print(f"Thumbnail: {data['thumbnail']}") print(f"SRT: {data['srtFile']}") ``` ```javascript JavaScript theme={null} const jobId = '265a7c1a-4985-4058-9208-68114f131a2b'; async function pollRenderJob(apiKey, jobId, maxWait = 900000, pollInterval = 15000) { const url = `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`; const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const response = await fetch(url, { method: 'GET', headers: { 'Authorization': apiKey, 'accept': 'application/json' } }); const data = await response.json(); if (!data.success) { console.log(`Job failed: ${data.data?.error_message || 'Unknown error'}`); return data; } const status = data.data?.status; if (status === 'in-progress') { const progress = data.data?.renderProgress || 0; const message = data.data?.renderProgressMessage || 'Processing'; console.log(`Rendering... ${progress}% - ${message}`); await new Promise(resolve => setTimeout(resolve, pollInterval)); continue; } if (status === 'completed') { console.log('Render complete!'); console.log(`Video URL: ${data.data.videoURL}`); console.log(`Duration: ${data.data.videoDuration}s`); return data; } if (status === 'failed') { console.log(`Render failed: ${data.data?.error_message}`); return data; } console.log(`Unexpected status: ${status}`); return data; } console.log('Timeout waiting for render'); return null; } // Usage const result = await pollRenderJob('YOUR_API_KEY', jobId); if (result?.success && result.data?.status === 'completed') { console.log(`\nVideo: ${result.data.videoURL}`); console.log(`Share: ${result.data.videoShareURL}`); } ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) func pollRenderJob(apiKey, jobID string, maxWait, pollInterval time.Duration) (map[string]interface{}, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) startTime := time.Now() for time.Since(startTime) < maxWait { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var data map[string]interface{} json.Unmarshal(body, &data) success, _ := data["success"].(bool) if !success { fmt.Println("Job failed") return data, nil } jobData, _ := data["data"].(map[string]interface{}) status, _ := jobData["status"].(string) if status == "in-progress" { progress, _ := jobData["renderProgress"].(float64) fmt.Printf("Rendering... %.0f%%\n", progress) time.Sleep(pollInterval) continue } if status == "completed" { fmt.Println("Render complete!") if videoURL, ok := jobData["videoURL"].(string); ok { fmt.Printf("Video URL: %s\n", videoURL) } return data, nil } if status == "failed" { if msg, ok := jobData["error_message"].(string); ok { fmt.Printf("Render failed: %s\n", msg) } return data, nil } fmt.Printf("Unexpected status: %s\n", status) return data, nil } return nil, fmt.Errorf("timeout waiting for render") } func main() { result, err := pollRenderJob( "YOUR_API_KEY", "265a7c1a-4985-4058-9208-68114f131a2b", 15*time.Minute, 15*time.Second, ) if err != nil { fmt.Println("Error:", err) return } if success, ok := result["success"].(bool); ok && success { data := result["data"].(map[string]interface{}) if videoURL, ok := data["videoURL"].(string); ok { fmt.Printf("\nVideo: %s\n", videoURL) } } } ``` *** ## Render Progress Stages The render job progresses through the following stages: | Stage | `status` | `renderState` | Description | | -------------- | ------------- | ------------- | --------------------------------------------------------------------- | | **Queued** | `in-progress` | — | Job is queued; pre-processing has not started | | **Processing** | `in-progress` | `RUNNING` | Pre-render processing (scene composition, audio mixing) | | **Rendering** | `in-progress` | `RUNNING` | Video frame rendering is in progress | | **Completed** | `completed` | — | Video is ready for download | | **Failed** | `failed` | — | Render has failed; check `error_code` and `error_message` for details | *** ## Polling Best Practices Use a polling interval of **10–30 seconds** when checking job status. Polling too frequently may result in rate limiting. 1. **Use webhooks when possible.** Configure a `webhook` URL in the original render request to receive automatic notification when the job completes, rather than polling. 2. **Monitor renderProgress.** Use the `renderProgress` field to display progress to users. This field provides a percentage value from 0 to 100. 3. **Implement timeouts.** Set a maximum wait time based on expected video length. Longer videos require more rendering time. 4. **Cache results.** Once a render job completes, store the output URLs locally. Completed job data may be cleaned after a retention period. 5. **Handle failures gracefully.** Inspect `error_code` and `error_message` in failed responses to determine whether the issue is recoverable (for example, an invalid voice name can be corrected and the request resubmitted). # Generate Signed URL for File Upload Source: https://docs.pictory.ai/api-reference/media-management/generate-signed-url POST https://api.pictory.ai/pictoryapis/v1/media/generateurl Generate a pre-signed URL for secure file upload to storage. This endpoint provides a temporary URL that allows direct upload of media files without exposing credentials. ## Overview Generate a pre-signed URL that enables secure, direct upload of media files to storage without exposing your credentials. The signed URL is temporary and automatically expires after 24 hours. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## How to Generate a Signed URL and Upload a File ### Step 1: Generate a Signed URL Request a signed URL from the API to obtain temporary upload credentials. ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v1/media/generateurl \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "fileName": "sample1.mp3", "contentType": "audio/mpeg" }' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/media/generateurl" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileName": "sample1.mp3", "contentType": "audio/mpeg" } response = requests.post(url, json=payload, headers=headers) data = response.json() signed_url = data["data"]["signedUrl"] ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/media/generateurl', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: 'sample1.mp3', contentType: 'audio/mpeg' }) }); const data = await response.json(); const signedUrl = data.data.signedUrl; ``` ### Step 2: Upload Your File Use the signed URL to upload your file directly to storage via a PUT request. ```bash cURL theme={null} curl --location --request PUT 'SIGNED_URL_FROM_STEP_1' \ --header 'Content-Type: audio/mpeg' \ --data-binary '@/path/to/your/sample1.mp3' ``` ```python Python theme={null} import requests with open('/path/to/your/sample1.mp3', 'rb') as file: response = requests.put( signed_url, data=file, headers={'Content-Type': 'audio/mpeg'} ) if response.status_code == 200: print("File uploaded successfully!") ``` ```javascript JavaScript theme={null} const fileInput = document.querySelector('input[type="file"]'); const file = fileInput.files[0]; const uploadResponse = await fetch(signedUrl, { method: 'PUT', headers: { 'Content-Type': 'audio/mpeg' }, body: file }); if (uploadResponse.ok) { console.log('File uploaded successfully!'); } ``` ## Request ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Body Parameters Name of the file to be uploaded (e.g., `sample1.mp3`) MIME type of the file. Common types include: * `audio/mpeg` - MP3 audio files * `video/mp4` - MP4 video files * `image/jpeg` - JPEG images * `image/png` - PNG images * `audio/wav` - WAV audio files ## Response Indicates whether the request was successful The temporary pre-signed URL for file upload. Use this URL in a PUT request to upload your file. Expires after 24 hours. The permanent CloudFront CDN URL for accessing the uploaded file. Use this URL to reference or download the file in subsequent API calls. The unique storage key identifying the file location in the storage system. ```json 200 - Success theme={null} { "success": true, "data": { "signedUrl": "https://pictory-api-prod.s3.us-east-2.amazonaws.com/public/teams/aa46778d-7965-46e0-967d-b48ee5d6ead9/users/Google_100807387384973267064/47ca856b-a15a-41b8-9a4b-88386cfa77d9/test-sample.mp3?Content-Type=audio%2Fmpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIA2GVAALU3KROYHJW4%2F20251223%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20251223T061537Z&X-Amz-Expires=86400&X-Amz-Security-Token=...", "url": "https://dg516uv4r33q1.cloudfront.net/public/teams/aa46778d-7965-46e0-967d-b48ee5d6ead9/users/Google_100807387384973267064/47ca856b-a15a-41b8-9a4b-88386cfa77d9/test-sample.mp3", "key": "public/teams/aa46778d-7965-46e0-967d-b48ee5d6ead9/users/Google_100807387384973267064/47ca856b-a15a-41b8-9a4b-88386cfa77d9/test-sample.mp3" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": "Internal Server Error", "message": "An unexpected error occurred" } ``` ## Important Notes **Signed URL Expiration**: The signed URL automatically expires after 24 hours (86400 seconds). Upload your file promptly after receiving the URL to avoid expiration. **Content-Type Matching**: The `Content-Type` header in your upload request must exactly match the `contentType` specified when generating the signed URL, otherwise the upload will fail. After successfully uploading your file, use the permanent `url` field from the response to access or reference the file in future API calls. Store this URL for later use. ## Additional Resources For more information on working with pre-signed URLs, refer to the [AWS S3 Pre-signed URL documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/s3_example_s3_Scenario_PresignedUrl_section.html). # Search Videos and Images Source: https://docs.pictory.ai/api-reference/media-management/search-media GET https://api.pictory.ai/pictoryapis/v1/media/search Search for stock videos and images from integrated media libraries. Returns preview URLs, thumbnails, and metadata for matching media assets. ## Overview Search through Pictory's integrated stock media libraries to find relevant videos and images for your projects. Results include preview URLs, thumbnails, duration information, and descriptive metadata from sources like StoryBlocks and Getty Images. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/media/search ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters Search keyword for which media (video/image) results are required Example: `business`, `technology`, `nature` Filter results by category. Accepted values: * `Agriculture and Forestry` * `Arts and Entertainment` * `Autos and Vehicles` * `Beauty and Fitness` * `Biological Sciences` * `Books and Literature` * `Business and Industrial` * `Computers and Electronics` * `Finance` * `Food and Drink` * `Games` * `Health` * `Home and Garden` * `Internet and Telecom` * `Jobs and Education` * `People and Society` * `Pets and Animals` * `Real Estate` * `Science` * `Sports` * `Travel` Language of the search keyword. Defaults to `en` (English) *** ## Response Current page number of search results Array of media search results Unique identifier for the media asset Type of media: `video` or `image` Duration of the media in seconds (for video assets) Descriptive text about the media content Preview URLs for the media URL of the preview video or image URL of the preview thumbnail image (JPEG) Thumbnail URLs for the media URL of the thumbnail video or image URL of the thumbnail image (JPEG) The search keyword that matched this result Source library for the media (e.g., `story_blocks`) ### Response Examples ```json 200 - Success theme={null} { "data": { "page": 1, "searchResults": [ { "assetId": 10900066, "duration": 15, "id": 10900066, "keyword": "business", "mediaDescription": "Young Team Brainstorming Ideas On Sticky Notes. Colleagues Approve. Business Success Concept.", "mediaType": "video", "preview": { "jpg": "https://dm0qx8t0i9gc9.cloudfront.net/thumbnails/video/V1xq1AADx/videoblocks-young-business-team-brainstorming-ideas-on-sticky-notes_thumbnail-180_02.jpg", "url": "https://dm0qx8t0i9gc9.cloudfront.net/watermarks/video/V1xq1AADx/videoblocks-young-business-team_P480.mp4" }, "searchLibrary": "story_blocks", "thumbnail": { "jpg": "https://dm0qx8t0i9gc9.cloudfront.net/thumbnails/video/V1xq1AADx/videoblocks-young-business-team_thumbnail-180_02.jpg", "url": "https://dm0qx8t0i9gc9.cloudfront.net/watermarks/video/V1xq1AADx/videoblocks-young-business-team_P180.mp4" } } ] } } ``` ```json 400 - Bad Request theme={null} { "error": { "error_code": "INVALID_REQUEST", "error_message": "Request validation failed.", "fields": [ { "errors": "keyword is required", "name": "keyword" } ] }, "success": false } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/media/search?keyword=business&language=en' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests # Search for business-related videos url = "https://api.pictory.ai/pictoryapis/v1/media/search" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } params = { "keyword": "business meeting", "category": "Business and Industrial", "language": "en" } response = requests.get(url, headers=headers, params=params) data = response.json() # Process the results for result in data["data"]["searchResults"]: print(f"Asset ID: {result['assetId']}") print(f"Type: {result['mediaType']}") print(f"Description: {result['mediaDescription']}") print(f"Duration: {result.get('duration', 'N/A')} seconds") print(f"Preview URL: {result['preview']['url']}") print("---") ``` ```javascript JavaScript / Node.js theme={null} // Search for business-related videos const searchMedia = async (keyword, category) => { const url = new URL('https://api.pictory.ai/pictoryapis/v1/media/search'); url.searchParams.append('keyword', keyword); url.searchParams.append('category', category); url.searchParams.append('language', 'en'); const response = await fetch(url, { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } }); const data = await response.json(); // Process the results data.data.searchResults.forEach(result => { console.log(`Asset ID: ${result.assetId}`); console.log(`Type: ${result.mediaType}`); console.log(`Description: ${result.mediaDescription}`); console.log(`Duration: ${result.duration || 'N/A'} seconds`); console.log(`Preview URL: ${result.preview.url}`); console.log('---'); }); return data.data.searchResults; }; // Use the function await searchMedia('business meeting', 'Business and Industrial'); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" ) type Preview struct { URL string `json:"url"` JPG string `json:"jpg"` } type SearchResult struct { AssetID int `json:"assetId"` MediaType string `json:"mediaType"` Duration int `json:"duration,omitempty"` MediaDescription string `json:"mediaDescription"` Preview Preview `json:"preview"` Thumbnail Preview `json:"thumbnail"` Keyword string `json:"keyword"` SearchLibrary string `json:"searchLibrary"` } type SearchResponse struct { Data struct { Page int `json:"page"` SearchResults []SearchResult `json:"searchResults"` } `json:"data"` } func searchMedia(apiKey, keyword, category, language string) (*SearchResponse, error) { baseURL := "https://api.pictory.ai/pictoryapis/v1/media/search" params := url.Values{} params.Add("keyword", keyword) params.Add("language", language) if category != "" { params.Add("category", category) } fullURL := fmt.Sprintf("%s?%s", baseURL, params.Encode()) req, err := http.NewRequest("GET", fullURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result SearchResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { data, err := searchMedia("YOUR_API_KEY", "business meeting", "Business and Industrial", "en") if err != nil { panic(err) } // Process the results for _, result := range data.Data.SearchResults { fmt.Printf("Asset ID: %d\n", result.AssetID) fmt.Printf("Type: %s\n", result.MediaType) fmt.Printf("Description: %s\n", result.MediaDescription) if result.Duration > 0 { fmt.Printf("Duration: %d seconds\n", result.Duration) } fmt.Printf("Preview URL: %s\n", result.Preview.URL) fmt.Println("---") } } ``` *** ## Usage Notes **Media Sources**: Search results are sourced from integrated stock media libraries including StoryBlocks and Getty Images. All preview URLs include watermarks for copyright protection. Use the `category` parameter to filter results by topic and receive more relevant media that matches your specific use case and content theme. Preview URLs contain time-sensitive security tokens that expire. Access or download media assets promptly after retrieval to avoid broken links. # Get Music Genre Groups Source: https://docs.pictory.ai/api-reference/music-search/get-music-genre-groups GET https://api.pictory.ai/pictoryapis/v1/music/genres/groups Retrieve a list of high-level music genre groups for categorizing and filtering music ## Overview Retrieve a list of high-level music genre groups that categorize the more specific genres into broader musical families. Use these genre groups to provide simplified filtering options or to organize genres hierarchically in your application. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/music/genres/groups ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of high-level music genre group names. Each group represents a broad musical category that encompasses multiple specific genres. *** ## Response Examples ```json 200 - Success theme={null} [ "Cinematic", "Hip Hop", "Electronic", "Blues & Jazz", "Folk", "Rock", "Country", "Dance", "World", "Orchestral", "R&B & Soul", "Pop" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/music/genres/groups \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/genres/groups" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) genre_groups = response.json() # Print all available genre groups for group in genre_groups: print(group) ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/genres/groups', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const genreGroups = await response.json(); console.log('Available genre groups:', genreGroups); ``` ## Usage Notes Genre groups provide a simplified way to categorize music by broad styles. Use them to create top-level navigation or filtering, then combine with the specific genres endpoint for detailed selection. **Hierarchical Organization**: These 12 genre groups encompass the 100+ specific genres available through the `/music/genres` endpoint, providing a logical hierarchy for music organization. ## Common Use Cases ### 1. Create Hierarchical Genre Navigation Build a two-level genre selection interface with groups and specific genres: ```python theme={null} import requests headers = {"Authorization": "YOUR_API_KEY"} # Get genre groups groups = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres/groups", headers=headers ).json() # Get all specific genres all_genres = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres", headers=headers ).json() # Create hierarchical structure genre_hierarchy = { "Rock": [g for g in all_genres if 'Rock' in g and 'Folk' not in g], "Electronic": [g for g in all_genres if any(x in g for x in ['Electronic', 'House', 'Techno', 'Trance'])], "Blues & Jazz": [g for g in all_genres if 'Jazz' in g or 'Blues' in g], # ... continue for other groups } ``` ### 2. Populate Multi-Level Dropdown Create a grouped dropdown selector for better user experience: ```javascript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/genres/groups', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const genreGroups = await response.json(); // Create optgroup structure for HTML select const genreGroupOptions = genreGroups.map(group => ({ label: group, value: group, type: 'group' })); // Use in HTML /* */ ``` ### 3. Filter Music by Broad Category Allow users to quickly filter music by general style before refining: ```python theme={null} def get_music_by_genre_group(group_name, api_key): """ Get all specific genres within a genre group """ headers = {"Authorization": api_key} # Get all genres all_genres = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres", headers=headers ).json() # Map groups to their genres group_mapping = { "Rock": lambda g: 'Rock' in g and 'Folk' not in g, "Electronic": lambda g: any(x in g for x in ['Electronic', 'House', 'Techno', 'Trance', 'Dubstep']), "Blues & Jazz": lambda g: 'Jazz' in g or 'Blues' in g, "Hip Hop": lambda g: 'Hip Hop' in g or g in ['Trap', 'Gangsta', 'Drill'], "World": lambda g: any(x in g for x in ['African', 'Asian', 'Brazilian', 'Cuban', 'Indian']), # ... other mappings } if group_name in group_mapping: return [g for g in all_genres if group_mapping[group_name](g)] return [] # Example usage rock_genres = get_music_by_genre_group("Rock", "YOUR_API_KEY") print(f"Rock genres: {rock_genres}") ``` ### 4. Build Genre Explorer UI Create an interactive genre exploration interface: ```javascript theme={null} async function buildGenreExplorer() { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/genres/groups', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const groups = await response.json(); // Create tabs for each genre group const genreExplorer = groups.map(group => ({ groupName: group, displayName: group, icon: getIconForGroup(group), // Custom function to assign icons color: getColorForGroup(group) // Custom function for color coding })); return genreExplorer; } function getIconForGroup(group) { const icons = { 'Cinematic': '🎬', 'Hip Hop': '🎤', 'Electronic': '🎹', 'Blues & Jazz': '🎷', 'Rock': '🎸', 'Orchestral': '🎻', 'Pop': '🎵', 'World': '🌍' }; return icons[group] || '🎶'; } ``` ### 5. Validate and Map User Selections Ensure user selections map correctly to genre groups: ```python theme={null} import requests def validate_genre_group(user_selection, api_key): """ Validate if a user's selection is a valid genre group """ headers = {"Authorization": api_key} valid_groups = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres/groups", headers=headers ).json() if user_selection in valid_groups: return True, f"Valid group: {user_selection}" else: return False, f"Invalid group. Choose from: {', '.join(valid_groups)}" # Example is_valid, message = validate_genre_group("Rock", "YOUR_API_KEY") print(message) ``` ## Genre Group Descriptions Here's what each genre group typically encompasses: * **Cinematic**: Film scores, dramatic compositions, and soundtrack-style music * **Hip Hop**: Rap, trap, gangsta, and hip hop variations * **Electronic**: House, techno, trance, dubstep, and electronic dance music * **Blues & Jazz**: All jazz styles, blues variations, and related genres * **Folk**: Folk music, singer-songwriter, and acoustic styles * **Rock**: All rock subgenres from classic to alternative * **Country**: Country music, bluegrass, and Americana * **Dance**: Upbeat dance music, disco, and club-oriented tracks * **World**: International and ethnic music from various cultures * **Orchestral**: Classical, symphonic, and orchestral compositions * **R\&B & Soul**: Rhythm and blues, soul, and related urban music * **Pop**: Popular music, contemporary hits, and pop variations # Get Music Genres Source: https://docs.pictory.ai/api-reference/music-search/get-music-genres GET https://api.pictory.ai/pictoryapis/v1/music/genres Retrieve a list of available music genres for filtering and searching music tracks ## Overview Retrieve a comprehensive list of available music genres for filtering and searching music tracks. Use these genre values to find music that matches the specific musical style and sound of your video content. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/music/genres ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of available music genre names. Each genre represents a distinct musical style or category for filtering music tracks in search queries. *** ## Response Examples ```json 200 - Success theme={null} [ "Film Score (Electronic)", "Orchestral Hip Hop", "Dramedy", "Drone", "Horror", "Trip Hop", "Gypsy Jazz", "Polka", "Alternative Grunge", "Classic Rock", "Metal", "Indie Rock", "Pop Rock", "Punk Rock", "Acid Jazz", "Acoustic Blues", "Bebop", "Dixieland", "Electric Blues", "Exotica", "Jazz Ballad", "Jazz Fusion", "Latin Jazz", "Lounge", "Ragtime", "Smooth Jazz", "Swing", "Americana", "Bluegrass", "Country Folk", "Country Pop", "Country Rock", "Breakbeat", "Dance Pop", "Dubstep", "House", "Industrial", "Techno", "Trance", "Ambient", "Chill Out", "Downtempo", "Soft Rock", "Surf Rock", "African", "Brazilian / Samba", "Cuban / Salsa", "Indian", "Island", "Japanese", "Middle Eastern", "Reggae", "Reggaeton", "Tango", "Solo Piano", "Big Band", "Electro Rock", "Ska", "World Elements", "Synthwave", "Chiptune", "Acoustic", "Folk Pop", "Neo Soul", "R&B Pop", "Soul", "Big Beat", "Dancehall", "Afrobeat", "French", "European", "British", "New Wave", "Nu-Folk", "Lo-Fi", "Dub", "Drill", "Latin American", "Bachata", "Cumbia", "Rock-n-Roll", "Asian", "Celtic", "Spanish / Flamenco", "Bush Ballad", "Film Score (Orchestral)", "Folk Rock", "Gangsta", "Classic Hip Hop", "Trap", "20th Century", "Baroque", "Classical Period", "Romantic Period", "Adult Contemporary", "Bubblegum", "Easy Listening", "Electro Pop", "Indie Pop", "Singer-Songwriter", "Disco", "Funk", "Italian", "Klezmer", "Australiana" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/music/genres \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/genres" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) genres = response.json() # Print all available genres for genre in genres: print(genre) ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/genres', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const genres = await response.json(); console.log('Available genres:', genres); ``` ## Usage Notes Use these exact genre values when searching for music to ensure valid selection from available categories. Genre values are case-sensitive and must match exactly as returned by this endpoint. **Genre Diversity**: The API provides over 100 distinct genre options spanning diverse musical styles—from classical (Baroque, Romantic Period) to contemporary (Trap, Synthwave) to world music (African, Middle Eastern, Celtic)—enabling precise matching of music style to your video's theme. ## Common Use Cases ### 1. Populate a Genre Selector Fetch all available genres to populate a dropdown or selection interface in your application: ```python theme={null} genres = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres", headers={"Authorization": "YOUR_API_KEY"} ).json() # Use genres to populate UI dropdown genre_options = [{"label": genre, "value": genre} for genre in genres] ``` ### 2. Categorize Genres by Music Family Organize genres into broader musical categories for simplified user selection: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/music/genres', { headers: { 'Authorization': 'YOUR_API_KEY' } }); const genres = await response.json(); // Categorize genres const rockGenres = genres.filter(g => g.includes('Rock') && !g.includes('Folk') ); const jazzGenres = genres.filter(g => g.includes('Jazz') ); const classicalGenres = genres.filter(g => g.includes('Classical') || g.includes('Baroque') || g.includes('Romantic') ); const electronicGenres = genres.filter(g => ['House', 'Techno', 'Trance', 'Dubstep', 'Electro'].some(e => g.includes(e)) ); ``` ### 3. Validate User Selection Ensure user-selected genres are valid before using them in music search requests: ```python theme={null} valid_genres = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres", headers={"Authorization": "YOUR_API_KEY"} ).json() user_genre = "Synthwave" if user_genre in valid_genres: # Proceed with music search using this genre print(f"Searching for {user_genre} music...") else: print("Invalid genre selected") ``` ### 4. Display Genre Categories Create a user-friendly categorized display of genres: ```python theme={null} import requests genres = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/genres", headers={"Authorization": "YOUR_API_KEY"} ).json() # Organize by main category categories = { "Rock": [g for g in genres if 'Rock' in g and 'Folk' not in g], "Jazz": [g for g in genres if 'Jazz' in g], "Electronic": [g for g in genres if any(x in g for x in ['House', 'Techno', 'Electronic', 'Electro'])], "Classical": [g for g in genres if any(x in g for x in ['Classical', 'Baroque', 'Romantic', '20th Century'])], "Hip Hop": [g for g in genres if 'Hip Hop' in g or g in ['Trap', 'Gangsta', 'Drill']], "World": [g for g in genres if any(x in g for x in ['African', 'Asian', 'Brazilian', 'Cuban', 'Indian', 'Japanese', 'Middle Eastern', 'Reggae', 'Celtic', 'Spanish'])], "Country": [g for g in genres if 'Country' in g or g in ['Americana', 'Bluegrass']], "Pop": [g for g in genres if 'Pop' in g and 'Hip Hop' not in g] } for category, genre_list in categories.items(): print(f"\n{category}:") for genre in sorted(genre_list): print(f" - {genre}") ``` # Get Music Instruments Source: https://docs.pictory.ai/api-reference/music-search/get-music-instruments GET https://api.pictory.ai/pictoryapis/v1/music/instruments Retrieve a list of available music instruments for filtering and searching music tracks ## Overview Retrieve a comprehensive list of available music instruments for filtering and searching music tracks. Use these instrument values to find music featuring specific instrumental sounds that complement your video content. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/music/instruments ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of available music instrument names. Each instrument represents a distinct sound or instrumental category for filtering music tracks in search queries. *** ## Response Examples ```json 200 - Success theme={null} [ "Vocal - Sounds / FX", "Banjo", "Bass - Electric", "Bassoon", "Brass", "Celeste", "Cello", "Clarinet", "Dobro", "Dulcimer", "Ethnic", "Finger Snaps", "Flugelhorn", "Gamelan", "Glockenspiel", "Harmonica", "Harp", "Harpsichord", "Jaw Harp", "Kalimba", "Kazoo", "Keyboard", "Lute", "Mandolin", "Marimba", "Orchestra", "Organ", "Piano", "Recorder", "Rhodes", "Saxophone", "Scratching", "Sitar", "Steel Drums", "Strings", "Theremin", "Trombone", "Tuba", "Ukulele", "Vibraphone", "Vocoder", "Whistling", "Woodwind", "Xylophone", "Guitar - Acoustic", "Bagpipes / Celtic Pipes", "Bells / Tubular Bells", "Bass - Double", "Guitar - Electric", "French Horn", "Hammond Organ", "Oboe / Cor Anglais", "Orchestral Noise", "Pan Pipes", "Sleigh Bells", "Synth", "Tambourine", "Vocal - Choir", "Duduk", "Congas", "Tabla", "Melodica", "Saz", "Clavinet", "Pizzicato", "Guitar - Slide", "Guitar - Spanish", "Stompbox", "Castanets", "Music Box", "Bass - Synth", "Shaker", "Guitar - Lap & Pedal", "Koto", "Cowbell", "Electronic Drums", "Shakuhachi", "Shamisen", "Vinyl Crackle", "Hand Claps", "Flute / Piccolo", "Trumpet / Cornet", "Violin / Viola", "Whistles / Tin / Penny", "Erhu", "FX (Heartbeat)", "FX (Clock)", "FX (Reverse)", "Accordion / Bandoneon", "Cuatro", "Hand Drum", "Vocal - Lyrics", "Percussion", "Drum Kit", "Drum Kit - Brushes", "Didgeridoo" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/music/instruments \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/instruments" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) instruments = response.json() # Print all available instruments for instrument in instruments: print(instrument) ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/instruments', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const instruments = await response.json(); console.log('Available instruments:', instruments); ``` ## Usage Notes Use these exact instrument values when searching for music to ensure valid selection from available categories. Instrument values are case-sensitive and must match exactly as returned by this endpoint. **Instrument Diversity**: The API provides 95 distinct instrument options spanning traditional acoustic instruments (Piano, Guitar, Violin), electronic sounds (Synth, Vocoder, Electronic Drums), world instruments (Sitar, Koto, Didgeridoo), and special effects (FX Heartbeat, Vinyl Crackle, Hand Claps)—enabling precise matching of instrumental sound to your video's sonic requirements. ## Common Use Cases ### 1. Populate an Instrument Selector Fetch all available instruments to populate a dropdown or selection interface in your application: ```python theme={null} instruments = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/instruments", headers={"Authorization": "YOUR_API_KEY"} ).json() # Use instruments to populate UI dropdown instrument_options = [{"label": instrument, "value": instrument} for instrument in instruments] ``` ### 2. Categorize Instruments by Type Organize instruments into categories for simplified user selection: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/music/instruments', { headers: { 'Authorization': 'YOUR_API_KEY' } }); const instruments = await response.json(); // Categorize instruments by type const stringInstruments = instruments.filter(i => i.includes('Guitar') || i.includes('Violin') || i.includes('Cello') || i.includes('Strings') || i.includes('Harp') || i.includes('Banjo') ); const brassInstruments = instruments.filter(i => i.includes('Trumpet') || i.includes('Trombone') || i.includes('French Horn') || i.includes('Tuba') || i.includes('Brass') ); const percussionInstruments = instruments.filter(i => i.includes('Drum') || i.includes('Percussion') || i.includes('Tambourine') || i.includes('Cowbell') || i.includes('Shaker') ); const vocalInstruments = instruments.filter(i => i.includes('Vocal') || i.includes('Choir') ); const worldInstruments = instruments.filter(i => ['Sitar', 'Koto', 'Tabla', 'Didgeridoo', 'Duduk', 'Erhu', 'Shamisen'].includes(i) ); ``` ### 3. Validate User Input Ensure user-selected instruments are valid before using them in music search requests: ```python theme={null} valid_instruments = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/instruments", headers={"Authorization": "YOUR_API_KEY"} ).json() user_instrument = "Piano" if user_instrument in valid_instruments: # Proceed with music search using this instrument print(f"Searching for {user_instrument} music...") else: print("Invalid instrument selected") ``` ### 4. Build Instrument Filter Interface Create a multi-category instrument filter for advanced music search: ```python theme={null} import requests instruments = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/instruments", headers={"Authorization": "YOUR_API_KEY"} ).json() # Organize by instrument families categories = { "Strings": [i for i in instruments if any(x in i for x in ['Guitar', 'Violin', 'Viola', 'Cello', 'Strings', 'Bass', 'Harp', 'Banjo', 'Ukulele', 'Mandolin'])], "Brass": [i for i in instruments if any(x in i for x in ['Trumpet', 'Trombone', 'French Horn', 'Tuba', 'Brass', 'Flugelhorn'])], "Woodwind": [i for i in instruments if any(x in i for x in ['Flute', 'Clarinet', 'Oboe', 'Saxophone', 'Bassoon', 'Woodwind', 'Recorder'])], "Percussion": [i for i in instruments if any(x in i for x in ['Drum', 'Percussion', 'Tambourine', 'Cowbell', 'Shaker', 'Congas', 'Tabla'])], "Keyboard": [i for i in instruments if any(x in i for x in ['Piano', 'Keyboard', 'Organ', 'Rhodes', 'Synth', 'Harpsichord', 'Clavinet'])], "Vocal": [i for i in instruments if 'Vocal' in i or 'Choir' in i], "World": [i for i in instruments if any(x in i for x in ['Sitar', 'Koto', 'Didgeridoo', 'Duduk', 'Erhu', 'Shamisen', 'Tabla', 'Bagpipes', 'Pan Pipes'])], "Electronic": [i for i in instruments if any(x in i for x in ['Synth', 'Electronic', 'Vocoder', 'Scratching'])], "FX": [i for i in instruments if 'FX' in i or i in ['Vinyl Crackle', 'Hand Claps', 'Finger Snaps']] } for category, instrument_list in categories.items(): print(f"\n{category}:") for instrument in sorted(instrument_list): print(f" - {instrument}") ``` ### 5. Search by Multiple Instruments Combine instrument filtering with other search criteria: ```javascript theme={null} async function searchMusicByInstrument(instruments, mood, genre) { // Get valid instruments first const validInstrumentsResponse = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/instruments', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const validInstruments = await validInstrumentsResponse.json(); // Validate requested instruments const validatedInstruments = instruments.filter(i => validInstruments.includes(i) ); if (validatedInstruments.length === 0) { console.error('No valid instruments selected'); return []; } console.log(`Searching for ${mood} ${genre} music with instruments: ${validatedInstruments.join(', ')}`); // Use validated instruments in your music search // (This would integrate with a music search endpoint) } // Example usage await searchMusicByInstrument( ['Piano', 'Violin / Viola', 'Cello'], 'Calm / Serene', 'Classical Period' ); ``` # Get Music Moods Source: https://docs.pictory.ai/api-reference/music-search/get-music-moods GET https://api.pictory.ai/pictoryapis/v1/music/moods Retrieve a list of available music moods for filtering and searching music tracks ## Overview Retrieve a comprehensive list of available music moods for filtering and searching music tracks. Use these mood values to find music that precisely matches the emotional tone and atmosphere of your video content. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/music/moods ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of available music mood names. Each mood represents a distinct emotional or tonal quality for filtering music tracks in search queries. *** ## Response Examples ```json 200 - Success theme={null} [ "Passionate", "Funny / Silly", "Gentle / Soft", "Raw / Gritty", "Intense / Hard", "Hopeful / Optimistic", "Uplifting / Inspiring", "Sensual / Intimate", "Ominous / Looming", "Sad / Melancholic", "Sweet / Warm", "Patriotic", "Beautiful", "Dramatic", "Epic", "Exotic", "Romantic", "Abstract", "Bittersweet", "Bouncy", "Cheesy", "Emotional", "Fun", "Funky", "Haunting", "Joyful", "Moody", "Pulsing", "Punchy", "Swaggering", "Feel Good", "Majestic", "Grooving", "Playful / Whimsical", "Smooth / Flowing", "Cool", "Mysterious", "Sneaky", "Jazzy", "Driving", "Anthemic", "Neutral", "Aggressive", "Ethereal / Airy", "Happy / Bright", "Suspense / Tense", "Powerful / Strong", "Rowdy / Boisterous", "Dark / Brooding", "Building / Escalating", "Calm / Serene", "Quirky / Kitsch", "Celebration", "Upbeat / Cheerful", "Laid-back", "Confident", "Shimmering", "Sophisticated", "Energetic / Lively", "Exciting / Rousing", "Nostalgic / Reflective", "Strange / Weird", "Anxious / Fear" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/music/moods \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/moods" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) moods = response.json() # Print all available moods for mood in moods: print(mood) ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/moods', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const moods = await response.json(); console.log('Available moods:', moods); ``` ## Usage Notes Use these exact mood values when searching for music to ensure valid selection from available categories. Mood values are case-sensitive and must match exactly as returned by this endpoint. **Mood Categories**: The API provides over 60 distinct mood options spanning a comprehensive range of emotional tones—from "Calm / Serene" to "Intense / Hard"—enabling precise alignment of music with your video's emotional narrative. ## Common Use Cases ### 1. Populate a Mood Selector Fetch all available moods to populate a dropdown or selection interface in your application: ```python theme={null} moods = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/moods", headers={"Authorization": "YOUR_API_KEY"} ).json() # Use moods to populate UI dropdown mood_options = [{"label": mood, "value": mood} for mood in moods] ``` ### 2. Categorize Moods by Emotion Organize moods into emotional categories for simplified user selection: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/music/moods', { headers: { 'Authorization': 'YOUR_API_KEY' } }); const moods = await response.json(); // Filter moods by type const happyMoods = moods.filter(m => m.includes('Happy') || m.includes('Joyful') || m.includes('Cheerful') ); const calmMoods = moods.filter(m => m.includes('Calm') || m.includes('Serene') || m.includes('Gentle') ); ``` ### 3. Validate User Input Ensure user-selected moods are valid before using them in music search requests: ```python theme={null} valid_moods = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/moods", headers={"Authorization": "YOUR_API_KEY"} ).json() user_mood = "Uplifting / Inspiring" if user_mood in valid_moods: # Proceed with music search using this mood print(f"Searching for {user_mood} music...") else: print("Invalid mood selected") ``` # Get Music Purposes Source: https://docs.pictory.ai/api-reference/music-search/get-music-purposes GET https://api.pictory.ai/pictoryapis/v1/music/purposes Retrieve a list of available music purposes for filtering and searching music tracks ## Overview Retrieve a comprehensive list of available music purposes for filtering and searching music tracks. Use these purpose values to find music that matches the specific use case, context, or application of your video content. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/music/purposes ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of available music purpose names. Each purpose represents a distinct use case, context, or application scenario for filtering music tracks in search queries. *** ## Response Examples ```json 200 - Success theme={null} [ "TV Theme", "Heroic", "Nature / Beauty", "Action", "Adventure", "Comedy", "Drama", "Fantasy", "Horror", "Shopping", "Sports", "Thriller", "Wedding", "80s", "Educational", "News / Current Affairs", "On-Hold", "Road Trip", "Sci-Fi", "Time Lapse", "Aerobics / Fitness", "Atmospheric", "Branding / Identity", "Corporate / Business", "Children / Preschool", "Crime", "Landscape / Panorama", "Mystery / Tension", "Nightclub", "Technology", "Slow-Mo", "War / Military", "Western / Cowboy", "Bollywood", "Historical / Period", "Real Estate", "Christmas", "Surfing", "Detective / Spy", "Dramedy", "Game Show", "50s", "60s", "70s", "Chase Sequence", "Elevator Music", "Exotic", "Failure / Defeat", "Fairytale", "Fanfare", "Frantic / Busy", "Futuristic", "Lullaby", "Luxury", "Retro", "Romantic Comedy", "Sex / Erotic", "Lazy / Plodding", "Space / Cosmos", "Success / Victory", "Superhero", "Swampy / Outback", "Tribal", "Magical / Supernatural", "Simple / Minimal", "Adrenaline", "Evil / Villainous", "Religious / Church", "Theatre / Showtunes", "Regal / Royal", "Circus / Carnival", "Tropical", "Fashion / Beauty", "Animation / Explainer", "Trailer / Promo", "Yoga / Relaxation", "Meditation" ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/music/purposes \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/purposes" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) purposes = response.json() # Print all available purposes for purpose in purposes: print(purpose) ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/purposes', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const purposes = await response.json(); console.log('Available purposes:', purposes); ``` ## Usage Notes Use these exact purpose values when searching for music to ensure valid selection from available categories. Purpose values are case-sensitive and must match exactly as returned by this endpoint. **Purpose Diversity**: The API provides 74 distinct purpose options spanning various contexts—from media types (TV Theme, Animation/Explainer, Trailer/Promo) to genres (Action, Comedy, Horror) to specific applications (Corporate/Business, Wedding, Real Estate)—enabling precise matching of music to your video's intended use and context. ## Common Use Cases ### 1. Populate a Purpose Selector Fetch all available purposes to populate a dropdown or selection interface in your application: ```python theme={null} purposes = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/purposes", headers={"Authorization": "YOUR_API_KEY"} ).json() # Use purposes to populate UI dropdown purpose_options = [{"label": purpose, "value": purpose} for purpose in purposes] ``` ### 2. Categorize Purposes by Context Organize purposes into logical categories for simplified user selection: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v1/music/purposes', { headers: { 'Authorization': 'YOUR_API_KEY' } }); const purposes = await response.json(); // Categorize by type const mediaGenres = purposes.filter(p => ['Action', 'Adventure', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi'].includes(p) ); const businessPurposes = purposes.filter(p => p.includes('Corporate') || p.includes('Business') || p.includes('Branding') || p.includes('Real Estate') || p.includes('Technology') ); const timePerods = purposes.filter(p => ['50s', '60s', '70s', '80s', 'Retro', 'Futuristic', 'Historical / Period'].includes(p) ); const videoPurposes = purposes.filter(p => p.includes('Animation') || p.includes('Trailer') || p.includes('Time Lapse') || p.includes('Slow-Mo') || p.includes('Explainer') ); const eventPurposes = purposes.filter(p => ['Wedding', 'Christmas', 'Circus / Carnival', 'Game Show'].includes(p) ); const wellnessPurposes = purposes.filter(p => p.includes('Yoga') || p.includes('Meditation') || p.includes('Relaxation') || p.includes('Aerobics') || p.includes('Fitness') ); ``` ### 3. Validate User Input Ensure user-selected purposes are valid before using them in music search requests: ```python theme={null} valid_purposes = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/purposes", headers={"Authorization": "YOUR_API_KEY"} ).json() user_purpose = "Corporate / Business" if user_purpose in valid_purposes: # Proceed with music search using this purpose print(f"Searching for {user_purpose} music...") else: print("Invalid purpose selected") ``` ### 4. Build Multi-Category Purpose Filter Create a categorized purpose filter for intuitive music discovery: ```python theme={null} import requests purposes = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/purposes", headers={"Authorization": "YOUR_API_KEY"} ).json() # Organize by category categories = { "Film & TV": [p for p in purposes if any(x in p for x in ['TV Theme', 'Action', 'Adventure', 'Comedy', 'Drama', 'Fantasy', 'Horror', 'Thriller', 'Sci-Fi', 'Western', 'Detective', 'Spy'])], "Business & Corporate": [p for p in purposes if any(x in p for x in ['Corporate', 'Business', 'Branding', 'Identity', 'Technology', 'Real Estate', 'Educational', 'News'])], "Video Production": [p for p in purposes if any(x in p for x in ['Animation', 'Explainer', 'Trailer', 'Promo', 'Time Lapse', 'Slow-Mo'])], "Events & Occasions": [p for p in purposes if any(x in p for x in ['Wedding', 'Christmas', 'Circus', 'Carnival', 'Game Show', 'Shopping'])], "Time Periods": [p for p in purposes if p in ['50s', '60s', '70s', '80s', 'Retro', 'Historical / Period', 'Futuristic']], "Wellness & Lifestyle": [p for p in purposes if any(x in p for x in ['Yoga', 'Meditation', 'Relaxation', 'Aerobics', 'Fitness', 'Fashion', 'Beauty', 'Luxury'])], "Moods & Emotions": [p for p in purposes if any(x in p for x in ['Heroic', 'Success', 'Victory', 'Failure', 'Defeat', 'Adrenaline', 'Romantic', 'Lullaby'])], "Nature & Places": [p for p in purposes if any(x in p for x in ['Nature', 'Beauty', 'Landscape', 'Panorama', 'Space', 'Cosmos', 'Tropical', 'Swampy', 'Outback'])], "Cultural & Regional": [p for p in purposes if any(x in p for x in ['Bollywood', 'Western', 'Cowboy', 'Religious', 'Church', 'Tribal', 'Exotic'])], "Special Applications": [p for p in purposes if any(x in p for x in ['On-Hold', 'Elevator', 'Nightclub', 'Road Trip', 'Surfing', 'Chase Sequence'])] } for category, purpose_list in categories.items(): print(f"\n{category}:") for purpose in sorted(purpose_list): print(f" - {purpose}") ``` ### 5. Smart Purpose Recommendation Suggest relevant purposes based on video content analysis: ```javascript theme={null} async function recommendPurposes(videoKeywords) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/purposes', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const allPurposes = await response.json(); // Map keywords to relevant purposes const keywordMappings = { 'office': ['Corporate / Business', 'Technology', 'Educational'], 'wedding': ['Wedding', 'Romantic Comedy', 'Fashion / Beauty'], 'product': ['Branding / Identity', 'Corporate / Business', 'Shopping'], 'fitness': ['Aerobics / Fitness', 'Sports', 'Yoga / Relaxation'], 'travel': ['Road Trip', 'Adventure', 'Landscape / Panorama'], 'children': ['Children / Preschool', 'Fairytale', 'Animation / Explainer'], 'tech': ['Technology', 'Futuristic', 'Corporate / Business'], 'luxury': ['Luxury', 'Fashion / Beauty', 'Regal / Royal'], 'nature': ['Nature / Beauty', 'Landscape / Panorama', 'Time Lapse'] }; const recommendations = new Set(); videoKeywords.forEach(keyword => { const mapped = keywordMappings[keyword.toLowerCase()]; if (mapped) { mapped.forEach(purpose => { if (allPurposes.includes(purpose)) { recommendations.add(purpose); } }); } }); return Array.from(recommendations); } // Example usage const suggestions = await recommendPurposes(['office', 'tech', 'product']); console.log('Recommended purposes:', suggestions); // Output: ['Corporate / Business', 'Technology', 'Branding / Identity', ...] ``` ### 6. Filter by Time Period Create a dedicated filter for era-specific music: ```python theme={null} def get_time_period_purposes(api_key): """ Extract time period-specific purposes for historical or retro content """ headers = {"Authorization": api_key} all_purposes = requests.get( "https://api.pictory.ai/pictoryapis/v1/music/purposes", headers=headers ).json() time_periods = { "Classic Eras": ['50s', '60s', '70s', '80s'], "Historical": ['Historical / Period', 'Western / Cowboy'], "Retro & Vintage": ['Retro', 'Bollywood'], "Future": ['Futuristic', 'Sci-Fi', 'Space / Cosmos'] } filtered_periods = {} for category, keywords in time_periods.items(): filtered_periods[category] = [ p for p in all_purposes if p in keywords ] return filtered_periods # Example usage periods = get_time_period_purposes("YOUR_API_KEY") for category, purposes in periods.items(): print(f"{category}: {', '.join(purposes)}") ``` # Search Music Tracks Source: https://docs.pictory.ai/api-reference/music-search/search-music POST https://api.pictory.ai/pictoryapis/v1/music/search Search for music tracks based on various filters including genres, moods, instruments, purposes, and duration ## Overview Search through Pictory's music library to find tracks that match your specific criteria. Filter by multiple attributes including genre groups, genres, moods, instruments, purposes, duration, and search queries. Results include streaming URLs, metadata, and comprehensive categorization data. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/music/search ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Body Parameters Page number for pagination (must be ≥ 1) Number of results per page (must be between 20 and 100) Sort order for results **Options:** * `latest` - Sort by most recently added tracks * `shuffle` - Randomize the order of results * `featured` - Sort by featured/curated tracks Search query to filter tracks by keywords in title or description Filter by genre groups (e.g., \["Rock", "Electronic"]). Use the [Get Music Genre Groups](/api-reference/music-search/get-music-genre-groups) endpoint to retrieve valid values. Exclude specific genre groups from results Filter by specific genres (e.g., \["Indie Rock", "Techno"]). Use the [Get Music Genres](/api-reference/music-search/get-music-genres) endpoint to retrieve valid values. Exclude specific genres from results Filter by moods (e.g., \["Happy / Bright", "Energetic / Lively"]). Use the [Get Music Moods](/api-reference/music-search/get-music-moods) endpoint to retrieve valid values. Exclude specific moods from results Filter by instruments (e.g., \["Piano", "Guitar - Acoustic"]). Use the [Get Music Instruments](/api-reference/music-search/get-music-instruments) endpoint to retrieve valid values. Exclude specific instruments from results Filter by purposes (e.g., \["Corporate / Business", "Wedding"]). Use the [Get Music Purposes](/api-reference/music-search/get-music-purposes) endpoint to retrieve valid values. Exclude specific purposes from results Minimum track duration in seconds Maximum track duration in seconds *** ## Response Array of music track objects matching the search criteria Unique identifier for the music track Title of the music track Direct streaming URL for the audio track Track duration in seconds List of genres associated with the track List of instruments featured in the track List of moods/emotions conveyed by the track List of suggested use cases or purposes for the track Total number of tracks matching the search criteria across all pages Total number of pages available for the search results Current page number in the result set ## Response Examples ```json 200 - Success theme={null} { "items": [ { "id": 279, "title": "Bio Journey", "audioUrl": "https://tracks.melod.ie/track_versions/570/MEL142_06_1_Bio_Journey_%28Full%29_David_Godfrey_stream.mp3?1736295063", "duration": 161, "genres": ["Downtempo"], "instruments": [ "Bass - Electric", "Bass - Synth", "Guitar - Electric", "Keyboard", "Percussion", "Synth" ], "moods": [ "Calm / Serene", "Ethereal / Airy", "Feel Good", "Gentle / Soft", "Happy / Bright", "Hopeful / Optimistic", "Quirky / Kitsch", "Smooth / Flowing", "Sweet / Warm", "Upbeat / Cheerful" ], "purposes": [ "Atmospheric", "Corporate / Business", "Mystery / Tension", "Real Estate", "Simple / Minimal", "Technology", "Time Lapse" ] }, { "id": 165, "title": "Dusty Road Trip", "audioUrl": "https://tracks.melod.ie/track_versions/316/MEL119_01_1_Dusty_Road_Trip_%28Full%29_Marcos_H_Bolanos_stream.mp3?1725242063", "duration": 147, "genres": ["Country Rock", "Electric Blues"], "instruments": [ "Bass - Electric", "Drum Kit", "Guitar - Acoustic", "Guitar - Electric", "Harmonica", "Shaker" ], "moods": [ "Celebration", "Confident", "Energetic / Lively", "Exciting / Rousing", "Feel Good", "Fun", "Grooving", "Happy / Bright", "Hopeful / Optimistic", "Joyful", "Playful / Whimsical", "Punchy", "Rowdy / Boisterous", "Sweet / Warm", "Upbeat / Cheerful" ], "purposes": [ "Adventure", "Nature / Beauty", "Road Trip", "Western / Cowboy" ] } ], "totalItems": 1541, "totalPages": 78, "currentPage": 1 } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "pageSize", "errors": "pageSize must be greater than or equal to 20" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v1/music/search \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'accept: application/json' \ --data '{ "page": 1, "pageSize": 20, "query": "upbeat", "mood": ["Happy / Bright", "Energetic / Lively"], "purpose": ["Corporate / Business"] }' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/search" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json", "accept": "application/json" } payload = { "page": 1, "pageSize": 20, "query": "upbeat", "mood": ["Happy / Bright", "Energetic / Lively"], "purpose": ["Corporate / Business"] } response = requests.post(url, json=payload, headers=headers) data = response.json() # Print results print(f"Found {data['totalItems']} tracks across {data['totalPages']} pages") for track in data['items']: print(f"\nTitle: {track['title']}") print(f"Duration: {track['duration']} seconds") print(f"Genres: {', '.join(track['genres'])}") print(f"Audio URL: {track['audioUrl']}") ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/search', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json', 'accept': 'application/json' }, body: JSON.stringify({ page: 1, pageSize: 20, query: 'upbeat', mood: ['Happy / Bright', 'Energetic / Lively'], purpose: ['Corporate / Business'] }) } ); const data = await response.json(); // Print results console.log(`Found ${data.totalItems} tracks across ${data.totalPages} pages`); data.items.forEach(track => { console.log(`\nTitle: ${track.title}`); console.log(`Duration: ${track.duration} seconds`); console.log(`Genres: ${track.genres.join(', ')}`); console.log(`Audio URL: ${track.audioUrl}`); }); ``` *** ## Usage Notes Use the negation filters (`notGenre`, `notMood`, `notInstrument`, `notPurpose`) to exclude specific attributes and refine your search results. This is particularly useful for finding tracks that match a mood but exclude certain instruments or genres. **Pagination Requirements**: The `pageSize` parameter must be between 20 and 100. If you need fewer results, simply use the first N items from the response array. **Filter Validation**: All filter values (genres, moods, instruments, purposes) are case-sensitive and must match exactly as returned by their respective list endpoints. Use the dedicated GET endpoints to retrieve valid filter values before searching. ## Common Use Cases ### 1. Search by Keyword Find tracks matching a specific search term: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/music/search" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } # Search for "corporate" themed music payload = { "page": 1, "pageSize": 20, "query": "corporate" } response = requests.post(url, json=payload, headers=headers) tracks = response.json() print(f"Found {tracks['totalItems']} corporate tracks") ``` ### 2. Filter by Multiple Criteria Combine multiple filters for precise results: ```javascript theme={null} const searchTracks = async (filters) => { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/search', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ page: 1, pageSize: 20, genre: ['Indie Rock', 'Pop Rock'], mood: ['Energetic / Lively', 'Happy / Bright'], instrument: ['Guitar - Electric', 'Drum Kit'], purpose: ['Corporate / Business', 'Branding / Identity'], minDuration: 60, maxDuration: 180 }) } ); const data = await response.json(); return data.items; }; // Get energetic rock tracks suitable for corporate use const tracks = await searchTracks(); console.log(`Found ${tracks.length} matching tracks`); ``` ### 3. Exclude Unwanted Attributes Use negation filters to refine results: ```python theme={null} # Find upbeat music WITHOUT specific moods or instruments payload = { "page": 1, "pageSize": 20, "mood": ["Upbeat / Cheerful", "Energetic / Lively"], "notMood": ["Dark / Brooding", "Sad / Melancholic"], "notInstrument": ["Violin / Viola", "Cello"], "purpose": ["Corporate / Business"] } response = requests.post( "https://api.pictory.ai/pictoryapis/v1/music/search", json=payload, headers={"Authorization": "YOUR_API_KEY", "Content-Type": "application/json"} ) tracks = response.json() print(f"Found {tracks['totalItems']} upbeat corporate tracks without strings") ``` ### 4. Duration-Based Search Find tracks within a specific duration range: ```python theme={null} def find_tracks_by_duration(min_sec, max_sec, purpose=None): """ Find music tracks within a specific duration range """ payload = { "page": 1, "pageSize": 50, "minDuration": min_sec, "maxDuration": max_sec } if purpose: payload["purpose"] = [purpose] response = requests.post( "https://api.pictory.ai/pictoryapis/v1/music/search", json=payload, headers={ "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } ) return response.json() # Find short corporate tracks (30-60 seconds) short_tracks = find_tracks_by_duration(30, 60, "Corporate / Business") print(f"Found {short_tracks['totalItems']} tracks between 30-60 seconds") # Find longer background tracks (3-5 minutes) long_tracks = find_tracks_by_duration(180, 300) print(f"Found {long_tracks['totalItems']} tracks between 3-5 minutes") ``` ### 5. Paginate Through Results Retrieve all pages of search results: ```javascript theme={null} async function getAllTracks(searchCriteria) { let allTracks = []; let currentPage = 1; let totalPages = 1; while (currentPage <= totalPages) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/search', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ ...searchCriteria, page: currentPage, pageSize: 100 // Maximum page size }) } ); const data = await response.json(); allTracks = allTracks.concat(data.items); totalPages = data.totalPages; currentPage++; console.log(`Retrieved page ${currentPage - 1} of ${totalPages}`); } return allTracks; } // Get all piano tracks const allPianoTracks = await getAllTracks({ instrument: ['Piano'] }); console.log(`Total piano tracks: ${allPianoTracks.length}`); ``` ### 6. Sort and Filter Combined Use sorting with filters for optimal results: ```python theme={null} # Get latest upbeat corporate tracks payload = { "page": 1, "pageSize": 20, "sort": "latest", "mood": ["Upbeat / Cheerful", "Energetic / Lively"], "purpose": ["Corporate / Business", "Branding / Identity"] } response = requests.post( "https://api.pictory.ai/pictoryapis/v1/music/search", json=payload, headers={ "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } ) latest_tracks = response.json() # Get featured tracks for variety payload["sort"] = "featured" response = requests.post( "https://api.pictory.ai/pictoryapis/v1/music/search", json=payload, headers={ "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } ) featured_tracks = response.json() ``` ### 7. Build a Smart Music Recommender Create recommendations based on video content: ```python theme={null} def recommend_music_for_video(video_keywords, video_duration, video_type): """ Recommend music based on video characteristics """ # Map video type to purpose purpose_mapping = { "corporate": ["Corporate / Business", "Branding / Identity"], "wedding": ["Wedding", "Romantic Comedy"], "travel": ["Adventure", "Road Trip", "Nature / Beauty"], "tech": ["Technology", "Futuristic"], "kids": ["Children / Preschool", "Educational"] } # Determine mood from keywords mood_mapping = { "happy": ["Happy / Bright", "Joyful", "Upbeat / Cheerful"], "calm": ["Calm / Serene", "Gentle / Soft"], "energetic": ["Energetic / Lively", "Exciting / Rousing"], "dramatic": ["Dramatic", "Intense / Hard"] } purposes = purpose_mapping.get(video_type, []) moods = [] for keyword in video_keywords: if keyword in mood_mapping: moods.extend(mood_mapping[keyword]) # Search with calculated criteria payload = { "page": 1, "pageSize": 20, "sort": "featured", "purpose": purposes, "mood": moods, "minDuration": int(video_duration * 0.8), # At least 80% of video duration "maxDuration": int(video_duration * 1.2) # At most 120% of video duration } response = requests.post( "https://api.pictory.ai/pictoryapis/v1/music/search", json=payload, headers={ "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } ) return response.json() # Example: Find music for 90-second corporate video with happy theme recommendations = recommend_music_for_video( video_keywords=["happy", "energetic"], video_duration=90, video_type="corporate" ) print(f"Recommended {len(recommendations['items'])} tracks") for track in recommendations['items'][:5]: print(f"- {track['title']} ({track['duration']}s)") ``` ### 8. Genre Group Exploration Search by high-level genre groups and drill down: ```javascript theme={null} async function exploreGenreGroup(genreGroup) { // First, search by genre group const groupResponse = await fetch( 'https://api.pictory.ai/pictoryapis/v1/music/search', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ page: 1, pageSize: 20, genreGroup: [genreGroup], sort: 'featured' }) } ); const groupTracks = await groupResponse.json(); // Analyze genres within this group const genresFound = new Set(); groupTracks.items.forEach(track => { track.genres.forEach(genre => genresFound.add(genre)); }); console.log(`Genre Group: ${genreGroup}`); console.log(`Total tracks: ${groupTracks.totalItems}`); console.log(`Specific genres found: ${Array.from(genresFound).join(', ')}`); return { totalTracks: groupTracks.totalItems, genres: Array.from(genresFound), sampleTracks: groupTracks.items }; } // Explore Electronic music const electronicMusic = await exploreGenreGroup('Electronic'); ``` # Delete Project Source: https://docs.pictory.ai/api-reference/projects/delete-project DELETE https://api.pictory.ai/pictoryapis/v2/projects/{projectid} Delete a specific project using its unique project ID. This action is permanent and cannot be undone. ## Overview Permanently delete a project from your Pictory account using its unique identifier. This action removes the project and all associated data, including video files, audio, subtitles, and configuration settings. **This action is permanent and cannot be undone.** Once deleted, the project and all its associated files will be permanently removed from your account. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} DELETE https://api.pictory.ai/pictoryapis/v2/projects/{projectid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the project to delete. Can be either a string (for v3 schema projects) or an integer (for v2 schema and earlier projects). Example: `20251222191648030d7df02f5b4054d4ca8831f1369459e25` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Indicates whether the deletion was successful Confirmation message about the deletion ## Response Examples ```json 200 - Success theme={null} { "success": true, "message": "Project deleted successfully" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "success": false, "message": "Project not found" } ``` ```json 403 - Forbidden theme={null} { "success": false, "message": "You do not have permission to delete this project" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request DELETE \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests project_id = "20251222191648030d7df02f5b4054d4ca8831f1369459e25" url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.delete(url, headers=headers) result = response.json() if result.get('success'): print(f"Project {project_id} deleted successfully") else: print(f"Failed to delete project: {result.get('message')}") ``` ```javascript JavaScript theme={null} const projectId = '20251222191648030d7df02f5b4054d4ca8831f1369459e25'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'DELETE', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const result = await response.json(); if (result.success) { console.log(`Project ${projectId} deleted successfully`); } else { console.log(`Failed to delete project: ${result.message}`); } ``` *** ## Usage Notes **Permanent Deletion**: Deleted projects cannot be recovered. Ensure you have backups of any important project files (videos, audio, subtitles) before deleting. Before deleting a project, consider using the [Get Project By Id](/api-reference/projects/get-project-by-id) endpoint to retrieve and back up all project assets (video, audio, subtitle files) for archival purposes. **Permission Requirements**: You can only delete projects that belong to your account or team. Attempting to delete projects from other accounts will result in a 403 Forbidden error. *** ## Common Use Cases ### 1. Delete Single Project with Confirmation Delete a project after user confirmation: ```python theme={null} import requests def delete_project_with_confirmation(project_id, api_key): """ Delete a project after confirming with the user """ # First, get project details get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" headers = {"Authorization": api_key} project_response = requests.get(get_url, headers=headers) if project_response.status_code != 200: print("Project not found") return False project = project_response.json() project_name = project.get('projectName', 'Unknown') # Confirm deletion print(f"Are you sure you want to delete '{project_name}'? (yes/no)") confirmation = input().strip().lower() if confirmation != 'yes': print("Deletion cancelled") return False # Delete project delete_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" delete_response = requests.delete(delete_url, headers=headers) result = delete_response.json() if result.get('success'): print(f"Project '{project_name}' deleted successfully") return True else: print(f"Failed to delete project: {result.get('message')}") return False # Example usage delete_project_with_confirmation("YOUR_PROJECT_ID", "YOUR_API_KEY") ``` ### 2. Bulk Delete Old Projects Delete multiple projects based on age criteria: ```python theme={null} from datetime import datetime, timedelta import requests def delete_old_projects(days_old, api_key, dry_run=True): """ Delete projects older than specified number of days """ headers = {"Authorization": api_key} # Get all projects list_url = "https://api.pictory.ai/pictoryapis/v2/projects" response = requests.get(list_url, headers=headers) projects = response.json().get('items', []) # Calculate cutoff date cutoff = datetime.now() - timedelta(days=days_old) # Find old projects old_projects = [] for project in projects: saved_date = datetime.fromisoformat(project['savedDate'].replace('Z', '+00:00')) if saved_date < cutoff: old_projects.append(project) print(f"Found {len(old_projects)} projects older than {days_old} days") if dry_run: print("DRY RUN - Projects that would be deleted:") for project in old_projects: print(f" - {project['name']} (saved: {project['savedDate']})") return # Delete old projects deleted_count = 0 for project in old_projects: delete_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project['id']}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.json().get('success'): print(f"Deleted: {project['name']}") deleted_count += 1 else: print(f"Failed to delete: {project['name']}") print(f"Deleted {deleted_count} out of {len(old_projects)} old projects") # Example usage - dry run first delete_old_projects(days_old=90, api_key="YOUR_API_KEY", dry_run=True) # Then execute for real # delete_old_projects(days_old=90, api_key="YOUR_API_KEY", dry_run=False) ``` ### 3. Delete Projects by Source Type Delete all projects created from a specific source: ```javascript theme={null} async function deleteProjectsBySource(sourceType, apiKey, confirmAll = false) { const headers = { 'Authorization': `${apiKey}` }; // Get all projects const listResponse = await fetch( 'https://api.pictory.ai/pictoryapis/v2/projects', { headers } ); const data = await listResponse.json(); // Filter by source type const matchingProjects = data.items.filter(p => p.source === sourceType); console.log(`Found ${matchingProjects.length} projects with source: ${sourceType}`); if (!confirmAll) { console.log('Set confirmAll=true to delete these projects'); return matchingProjects; } // Delete matching projects const results = { deleted: [], failed: [] }; for (const project of matchingProjects) { const deleteResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${project.id}`, { method: 'DELETE', headers } ); const result = await deleteResponse.json(); if (result.success) { results.deleted.push(project.name); console.log(`✓ Deleted: ${project.name}`); } else { results.failed.push(project.name); console.log(`✗ Failed: ${project.name}`); } } console.log(`\nDeleted: ${results.deleted.length}, Failed: ${results.failed.length}`); return results; } // Example usage const results = await deleteProjectsBySource('url', 'YOUR_API_KEY', true); ``` ### 4. Back Up Before Delete Back up project assets before deletion: ```python theme={null} import requests import os def backup_and_delete_project(project_id, api_key, backup_dir="./backups"): """ Backup project assets before deleting """ headers = {"Authorization": api_key} # Get project details get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) if response.status_code != 200: print("Project not found") return False project = response.json() project_name = project.get('projectName', project_id) # Create backup directory project_backup_dir = os.path.join(backup_dir, project_name) os.makedirs(project_backup_dir, exist_ok=True) print(f"Backing up project: {project_name}") # Backup assets assets = { 'video': project.get('videoURL'), 'audio': project.get('audioURL'), 'srt': project.get('srtFile'), 'vtt': project.get('vttFile'), 'txt': project.get('txtFile'), 'thumbnail': project.get('thumbnail') } backed_up = [] for asset_type, url in assets.items(): if url: try: file_response = requests.get(url) extension = asset_type if asset_type in ['srt', 'vtt', 'txt'] else url.split('.')[-1].split('?')[0] filename = f"{project_name}_{asset_type}.{extension}" filepath = os.path.join(project_backup_dir, filename) with open(filepath, 'wb') as f: f.write(file_response.content) backed_up.append(asset_type) print(f" ✓ Backed up {asset_type}") except Exception as e: print(f" ✗ Failed to backup {asset_type}: {e}") # Save project metadata import json metadata_path = os.path.join(project_backup_dir, "metadata.json") with open(metadata_path, 'w') as f: json.dump(project, f, indent=2) print(f"Backup complete: {len(backed_up)} assets saved to {project_backup_dir}") # Delete project print(f"Deleting project...") delete_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" delete_response = requests.delete(delete_url, headers=headers) result = delete_response.json() if result.get('success'): print(f"Project '{project_name}' deleted successfully") return True else: print(f"Failed to delete project: {result.get('message')}") return False # Example usage backup_and_delete_project("YOUR_PROJECT_ID", "YOUR_API_KEY") ``` ### 5. Safe Delete with Retry Logic Delete project with error handling and retry: ```javascript theme={null} async function safeDeleteProject(projectId, apiKey, maxRetries = 3) { const headers = { 'Authorization': `${apiKey}` }; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Attempt ${attempt} to delete project ${projectId}...`); const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'DELETE', headers } ); if (response.status === 401) { throw new Error('Unauthorized - check your API key'); } if (response.status === 404) { console.log('Project already deleted or does not exist'); return { success: true, message: 'Project not found' }; } const result = await response.json(); if (result.success) { console.log('Project deleted successfully'); return result; } if (attempt < maxRetries) { console.log(`Deletion failed, retrying in ${attempt * 2} seconds...`); await new Promise(resolve => setTimeout(resolve, attempt * 2000)); } } catch (error) { console.error(`Error on attempt ${attempt}:`, error.message); if (attempt === maxRetries) { throw error; } await new Promise(resolve => setTimeout(resolve, attempt * 2000)); } } throw new Error(`Failed to delete project after ${maxRetries} attempts`); } // Example usage try { const result = await safeDeleteProject('YOUR_PROJECT_ID', 'YOUR_API_KEY'); console.log('Result:', result); } catch (error) { console.error('Final error:', error.message); } ``` ### 6. Delete Projects in Specific Folder Delete all projects within a specific folder: ```python theme={null} def delete_folder_projects(folder_id, api_key, confirm=False): """ Delete all projects within a specific folder """ headers = {"Authorization": api_key} # Get projects in folder list_url = f"https://api.pictory.ai/pictoryapis/v2/projects?folder={folder_id}" response = requests.get(list_url, headers=headers) projects = response.json().get('items', []) print(f"Found {len(projects)} projects in folder {folder_id}") if not projects: print("No projects to delete") return # List projects for i, project in enumerate(projects, 1): print(f"{i}. {project['name']} (ID: {project['id']})") if not confirm: print("\nSet confirm=True to delete these projects") return # Delete projects deleted_count = 0 for project in projects: delete_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project['id']}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.json().get('success'): print(f"✓ Deleted: {project['name']}") deleted_count += 1 else: print(f"✗ Failed: {project['name']}") print(f"\nDeleted {deleted_count} out of {len(projects)} projects") # Example usage delete_folder_projects("your-folder-id", "YOUR_API_KEY", confirm=True) ``` *** ## Best Practices ### Safety Measures 1. **Always back up before deletion**: Use the Get Project By Id endpoint to retrieve all assets before deleting 2. **Implement confirmation**: Require user confirmation before executing delete operations 3. **Use dry runs**: Test deletion logic with dry run mode before executing 4. **Log deletions**: Keep audit logs of deleted projects for accountability 5. **Handle errors gracefully**: Implement proper error handling and retry logic ### Bulk Operations When deleting multiple projects: * Add delays between deletions to avoid rate limiting * Process deletions in batches * Implement comprehensive error handling * Provide progress updates to users * Log both successes and failures ### Recovery Planning Since deletion is permanent: * Maintain regular backups of important projects * Export project metadata before deletion * Download all media files (video, audio, subtitles) for archival * Keep deletion logs for audit trails # Get Project by ID Source: https://docs.pictory.ai/api-reference/projects/get-project-by-id GET https://api.pictory.ai/pictoryapis/v2/projects/{projectid} Retrieve detailed information about a specific project using its unique project ID ## Overview Retrieve comprehensive details about a specific project using its unique identifier. This endpoint returns the complete project configuration, including storyboard data, video URLs, audio settings, script information, and rendering details. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v2/projects/{projectid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the project to retrieve. Can be either a string (for v3 schema projects) or an integer (for v2 schema and earlier projects). Example: `20251222191648030d7df02f5b4054d4ca8831f1369459e25` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response The response contains detailed project information including configuration, media URLs, metadata, and all video elements such as scenes, backgrounds, text overlays, video clips, and audio tracks. *** ## Video Elements Structure A Pictory project contains multiple video elements organized hierarchically. Understanding this structure is essential for working with project data. **Do not modify project fields if their usage is unclear.** Incorrect modifications can cause unexpected behavior or corrupt your project. If you have questions about specific fields or their intended use, please contact our support team at [support@pictory.ai](mailto:support@pictory.ai) before making changes. ### Scenes The `scenes` array contains all scenes in the project. Each scene represents a segment of the video with its own visual content, text, and audio. **Scene Properties:** * `sceneId` - Unique identifier for the scene * `text` - The display text/narration for the scene * `sentence` - Array of text segments with formatting options (highlight, decoration, case) * `category` - Scene type: `TITLE`, `DEFAULT`, or `EMPHASIS` * `sortOrder` - Numeric order of the scene in the video * `durationAuto` - Auto-calculated duration in seconds * `keywords` - Array of highlighted keywords from the text * `layoutId` - Template layout applied to the scene ### Scene Text Styling (`styleData`) Each scene has a `styleData` object controlling text appearance: | Field | Description | | --------------------- | --------------------------------------------------------------------------- | | `fontName` | Font family name (e.g., `Arial`, `Plus Jakarta Sans`) | | `fontDisplayName` | Display name of the font | | `fontSize` | Font size in pixels | | `fontColor` | Text color in RGB format (e.g., `rgb(255,255,255)`) | | `fontWeight` | Font weight (e.g., `400` for normal, `700` for bold) | | `textAlign` | Text alignment: `left`, `center`, or `right` | | `keywordColor` | Highlight color for keywords in RGBA format | | `textBackgroundColor` | Background color behind text in RGBA format | | `textShadowColor` | Text shadow color in RGBA format | | `textShadowWidthFr` | Text shadow width as a fraction | | `maxLines` | Maximum number of text lines displayed | | `width` | Text container width as a fraction (0-1) | | `preset` | Text position preset: `bottom-left`, `bottom-center`, `center-center`, etc. | | `customFontId` | ID reference for custom uploaded fonts | | `animation` | Text animation settings (entry/exit animations) | ### Background Elements (`background`) Each scene has a `background` object that defines the visual backdrop. The background `type` determines the content: **Image Background** (`type: "image"`): ```json theme={null} { "type": "image", "elementData": { "url": "https://...", // Full resolution image URL "preview_jpg": "https://...", // Preview image URL "thumb": "https://...", // Thumbnail URL "thumb_jpg": "https://...", // Thumbnail JPG URL "library": "unsplash", // Source library (unsplash, story_blocks, getty, uploads) "libraryItemId": "mR1CIDduGLc", // Library item identifier "duration": 0 // Duration (0 for images) }, "styleData": { "kenBurns": "kb-364683-oqt0", // Ken Burns animation effect ID "imageZoomPan": true, // Enable zoom/pan animation "width": 0.7, // Display width as fraction "aspectRatio": 1.78, // Image aspect ratio "preset": "center-center", // Position preset "colorOverlay": { // Optional color overlay "hide": false, "bgColor": "rgb(0,37,60)", "opacity": 0.5 } } } ``` **Video Background** (`type: "video"`): ```json theme={null} { "type": "video", "elementData": { "url": "https://...", // Video URL (480p quality) "preview_jpg": "https://...", // Preview thumbnail image "thumb": "https://...", // Thumbnail video (180p) "thumb_jpg": "https://...", // Thumbnail image "library": "story_blocks", // Source: story_blocks, getty, pexels, uploads "libraryItemId": "120818", // Library item identifier "description": "Video title", // Video description "duration": 10 // Video duration in seconds }, "styleData": { "imageZoomPan": true // Enable zoom/pan animation }, "settings": { "loopVideo": true // Loop video playback } } ``` **Solid Color Background** (`type: "solid"`): ```json theme={null} { "type": "solid", "elementData": { "color": "rgb(69,123,113)" // Background color in RGB }, "styleData": {} } ``` ### Scene Elements (`elements`) The `elements` array within each scene contains overlay elements placed on top of the background. Each element has a `type` that determines its content. **Video Element** (`type: "video"`): Video clips placed as overlays within a scene: | Field | Description | | ---------------------------- | ------------------------------------------------------------ | | `elementId` | Unique identifier for the element | | `type` | Element type: `video` | | `isLogo` | Whether this video is used as a logo | | `hide` | Whether the element is hidden | | `settings.loopVideo` | Loop the video clip | | `settings.muteClipAudio` | Mute the video's audio | | `elementData.url` | Video file URL (480p quality) | | `elementData.preview_jpg` | Preview thumbnail image | | `elementData.thumb` | Thumbnail video URL (180p) | | `elementData.thumb_jpg` | Thumbnail image URL | | `elementData.library` | Source library: `story_blocks`, `getty`, `pexels`, `uploads` | | `elementData.libraryItemId` | Library item identifier | | `elementData.duration` | Video duration in seconds | | `styleData.top` | Vertical position (percentage from top) | | `styleData.left` | Horizontal position (percentage from left) | | `styleData.width` | Element width as fraction (0-1) | | `styleData.height` | Element height (null for auto) | | `styleData.aspectRatio` | Display aspect ratio | | `styleData.preset` | Position preset (null for custom position) | | `styleData.sourceDimensions` | Original source dimensions and offsets | | `styleData.colorOverlay` | Color overlay settings (bgColor, opacity, hide) | **Text Element** (`type: "text"`): Text overlays placed within a scene: | Field | Description | | ---------------------- | ------------------------------------------------------------- | | `elementId` | Unique identifier for the element | | `type` | Element type: `text` | | `componentName` | Component role: `main_title`, `story_text`, etc. | | `settings.textMode` | Mode: `useStoryText` (from scene) or `writeAnything` (custom) | | `elementData.sentence` | Array of text segments with formatting | **Text Sentence Structure:** ```json theme={null} { "sentence": [ { "text": "Hello World", "highlight": true, // Apply keyword highlighting "decoration": ["decor-bold", "decor-underline"], // Text decorations "case": "none" // Text case transformation } ] } ``` **Text Element Styling** (`styleData`): | Field | Description | | --------------------- | ------------------------------------- | | `fontName` | Font family | | `fontDisplayName` | Font display name | | `fontSize` | Font size in pixels | | `fontColor` | Text color (RGB format) | | `fontWeight` | Font weight (400=normal, 700=bold) | | `textAlign` | Alignment: `left`, `center`, `right` | | `keywordColor` | Highlight color (RGBA) | | `textBackgroundColor` | Background color (RGBA) | | `textShadowColor` | Shadow color (RGBA) | | `textShadowWidthFr` | Shadow width fraction | | `top` | Vertical position (null for preset) | | `left` | Horizontal position (null for preset) | | `width` | Container width (0-1) | | `maxLines` | Maximum display lines | | `fullWidth` | Use full width container | | `preset` | Position preset | | `customFontId` | Custom font reference | | `animation` | Animation configuration | **Text Animation Structure:** ```json theme={null} { "animation": { "textAnimation": [ { "name": "Typewriter", // Animation name "type": ["entry"], // Type: entry or exit "writingStyle": "character", // Style: character, word, or line "direction": "up", // Animation direction "speed": { "value": 1 } // Animation speed }, { "name": "None", "type": ["exit"] } ], "textBgAnimation": [] // Background animations } } ``` ### Audio Elements **Scene-Level Audio** (`scene.audios`): Each scene can have voiceover audio with word-level timing: | Field | Description | | ------------------------ | -------------------------------------------------- | | `audio_id` | Unique audio clip identifier | | `type` | Audio type: `voiceover` | | `voice` | Voice ID number | | `version` | Voice version (e.g., `v6`) | | `speed` | Playback speed percentage (100 = normal) | | `loudness` | Volume level (0-100) | | `clipUrl` | URL to the audio clip file | | `subScenes.duration` | Audio duration in seconds | | `subScenes.start` | Start time in milliseconds | | `subScenes.end` | End time in milliseconds | | `subScenes.word_markers` | Array of \[start\_ms, end\_ms, word] for each word | | `isAudioLinked` | Whether audio is linked across scenes | **Project-Level Audio** (`audios`): | Field | Description | | -------------- | ----------------------------------------- | | `audioId` | Unique audio identifier | | `type` | Type: `background` (music) or `voiceover` | | `name` | Audio/voice name | | `url` | Audio file URL | | `library` | Source: `melodie`, `elevenlabs`, `polly` | | `duration` | Total duration in seconds | | `loudness` | Volume level (0-100) | | `speed` | Playback speed percentage | | `language` | Language code (for voiceover) | | `word_markers` | Nested array of word timing per scene | | `time_markers` | Array of scene end timestamps (ms) | ### Media Libraries The `library` field indicates the source of visual content: | Library | Description | | -------------- | ------------------------------ | | `unsplash` | Unsplash stock photos | | `story_blocks` | Storyblocks stock video/images | | `getty` | Getty Images stock content | | `pexels` | Pexels stock photos/videos | | `melodie` | Melodie background music | | `elevenlabs` | ElevenLabs AI voices | | `polly` | Amazon Polly text-to-speech | | `uploads` | User-uploaded content | ## Response Examples ```json 200 - Success theme={null} { "project_id": "202601180951110906f5645f5952e4211bee28ac6100ddd68", "projectName": "AI Video Creation Guide", "projectType": "1", "source": "script", "step": "STORYBOARD", "schemaVersion": "v3", "layout": "16:9", "scriptLanguage": "en", "thumbnail": "https://cdn.example.com/thumbnail.jpg", "videoURL": "https://cdn.example.com/video/AI_Video_Creation_Guide.mp4", "audioURL": "https://cdn.example.com/audio/AI_Video_Creation_Guide.mp3", "shareVideoURL": "https://video.pictory.ai/project-id/share-id", "srtFile": "https://cdn.example.com/subtitles/subtitles.srt", "vttFile": "https://cdn.example.com/subtitles/subtitles.vtt", "txtFile": "https://cdn.example.com/subtitles/subtitles.txt", "videoDuration": 21.98, "audioSpeed": 100, "videoVolume": 50, "voiceOverId": "20260119024822426ae1e237bfddc4598b72be30c0c93ec33", "smartTemplateId": "20250813042630669e658e159aa60455692c1dad5473adtd3", "brandId": "", "saveDate": "2026-01-19T09:23:21.131Z", "generatedDate": "2026-01-19T09:26:23.059Z", "scriptTxtSceneSettings": { "autoVisualSelection": true, "selectedValueSceneBreakChar": "Both", "autoHighlightKeywords": true, "maxLines": 2 }, "visualFilter": { "libraries": [], "styles": [], "categories": [] }, "scenes": [ { "sceneId": "2026011809570315812dcab8bad504b84a76171a1dd620cc5", "projectId": "202601180951110906f5645f5952e4211bee28ac6100ddd68", "text": "Create Amazing AI Videos", "sentence": [ { "text": "Create " }, { "highlight": true, "text": "Amazing AI Videos" } ], "keywords": ["Amazing AI Videos"], "category": "TITLE", "sortOrder": 10001, "durationAuto": 5, "layoutId": "20250901134117bbb9d28f462ef350f3", "settings": { "hideText": true }, "styleData": { "fontName": "Plus Jakarta Sans", "fontDisplayName": "Plus Jakarta Sans", "fontSize": "24", "fontColor": "rgb(255,255,255)", "textAlign": "center", "keywordColor": "rgb(245,240,162)", "textBackgroundColor": "rgba(0,0,0,0.35)", "textShadowColor": "rgba(0,0,0,1)", "textShadowWidthFr": 0.03, "maxLines": 2, "width": 0.7, "preset": "bottom-center" }, "background": { "type": "image", "elementData": { "url": "https://images.example.com/photo.jpg", "preview_jpg": "https://images.example.com/photo.jpg", "thumb": "https://images.example.com/photo-thumb.jpg", "thumb_jpg": "https://images.example.com/photo-thumb.jpg", "library": "unsplash", "libraryItemId": "mR1CIDduGLc", "duration": 0 }, "styleData": { "kenBurns": "kb-364683-oqt0", "imageZoomPan": true, "width": 0.7, "aspectRatio": 1.78, "preset": "center-center", "colorOverlay": { "hide": false, "bgColor": "rgb(0,37,60)", "opacity": 0.5 } }, "settings": {} }, "elements": [ { "elementId": "2026011815294013646c92e549f6646c49f31274227faf54d", "type": "video", "isLogo": false, "hide": false, "elementData": { "url": "https://videos.example.com/clip.mp4", "preview_jpg": "https://videos.example.com/clip-preview.jpg", "thumb": "https://videos.example.com/clip-thumb.mp4", "thumb_jpg": "https://videos.example.com/clip-thumb.jpg", "library": "story_blocks", "libraryItemId": "0", "duration": 16 }, "settings": { "loopVideo": true, "muteClipAudio": true }, "styleData": { "top": 4.89, "left": 3.96, "width": 0.9259, "aspectRatio": 1.8066, "preset": null, "height": null, "sourceDimensions": { "width": 0.9512, "aspectRatio": 1.7778, "relYPercent": -0.0273, "relXPercent": -0.0127 }, "colorOverlay": { "hide": false, "bgColor": "rgb(110,111,132)", "opacity": 0.3 } } }, { "elementId": "202601180957034073d8a97be18614182b23c87f8c7fb2382", "type": "text", "componentName": "main_title", "elementData": { "sentence": [ { "text": "Create Amazing AI Videos", "decoration": ["decor-underline", "decor-bold"] } ] }, "settings": { "textMode": "writeAnything" }, "styleData": { "fontName": "Arial", "fontDisplayName": "Arial", "fontSize": "42", "fontColor": "rgb(255,255,255)", "fontWeight": 400, "textAlign": "left", "keywordColor": "rgba(248, 173, 151, 1)", "textBackgroundColor": "rgba(0,0,0,0)", "textShadowColor": "rgba(0,0,0,0)", "textShadowWidthFr": 0.03, "width": 0.9, "fullWidth": false, "preset": "bottom-left", "customFontId": null, "animation": { "textAnimation": [ { "name": "Typewriter", "type": ["entry"], "writingStyle": "character", "direction": "up", "speed": { "value": 1 } }, { "name": "None", "type": ["exit"] } ], "textBgAnimation": [] } } } ], "audios": [ { "audio_id": "20260119024821485409b9abeb6c8b4b4cae9357ecbe637cfd67", "type": "voiceover", "voice": 3036, "version": "v6", "speed": 100, "loudness": 50, "clipUrl": "https://audios.example.com/voiceover.mp3", "isAudioLinked": true, "subScenes": { "duration": 1.53, "start": 0, "end": 1533, "word_markers": [ [0, 300, "Create"], [350, 700, "Amazing"], [750, 950, "AI"], [1000, 1533, "Videos"] ] } } ] }, { "sceneId": "202601180957031586203f72337a9493893722a4b07b5af7b", "text": "Transform your content with powerful AI tools", "sentence": [ { "text": "Transform your content", "highlight": true, "decoration": ["decor-bold", "decor-underline"], "case": "none" }, { "text": " with powerful ", "decoration": ["decor-bold", "decor-underline"], "case": "none" }, { "text": "AI tools", "highlight": true, "decoration": ["decor-bold", "decor-underline"], "case": "none" } ], "keywords": ["Transform your content", "AI tools"], "category": "DEFAULT", "sortOrder": 10002, "settings": { "hideText": false }, "background": { "type": "solid", "elementData": { "color": "rgb(69,123,113)" }, "styleData": {} }, "elements": [] } ], "audios": [ { "audioId": "202601180957049641f56b14fa78641359f69ccb5e03aac8c", "type": "background", "name": "Open Arms", "library": "melodie", "url": "https://tracks.example.com/background-music.mp3", "duration": 191, "loudness": 10, "speed": 100 }, { "audioId": "20260119024822426ae1e237bfddc4598b72be30c0c93ec33", "type": "voiceover", "name": "Abby", "library": "elevenlabs", "language": "en-US", "url": "https://audios.example.com/voiceover-full.mp3", "duration": 15.226, "loudness": 50, "speed": 100, "version": "v6", "time_markers": [1533, 3874, 8072, 9268, 10715, 12301, 13446, 15226], "word_markers": [ [[0, 300, "Create"], [350, 700, "Amazing"], [750, 950, "AI"], [1000, 1533, "Videos"]], [[1533, 2100, "Transform"], [2150, 2400, "your"], [2450, 2900, "content"], [2950, 3200, "with"], [3250, 3600, "powerful"], [3650, 3774, "AI"], [3800, 4200, "tools"]] ] } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Project not found" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests project_id = "20251222191648030d7df02f5b4054d4ca8831f1369459e25" url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) project = response.json() # Print key project details print(f"Project: {project['projectName']}") print(f"Status: {project['step']}") print(f"Duration: {project['videoDuration']} seconds") print(f"Video URL: {project.get('videoURL', 'Not available')}") ``` ```javascript JavaScript theme={null} const projectId = '20251222191648030d7df02f5b4054d4ca8831f1369459e25'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const project = await response.json(); // Print key project details console.log(`Project: ${project.projectName}`); console.log(`Status: ${project.step}`); console.log(`Duration: ${project.videoDuration} seconds`); console.log(`Video URL: ${project.videoURL || 'Not available'}`); ``` *** # List Projects Source: https://docs.pictory.ai/api-reference/projects/get-projects GET https://api.pictory.ai/pictoryapis/v2/projects Retrieve a list of all projects associated with your Pictory account ## Overview Retrieve a list of all projects associated with your Pictory account. Projects represent video creation workflows at various stages, including those created from scripts, video/audio transcriptions, or URLs. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v2/projects ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters Pagination token for retrieving the next page of results. Pass this value exactly as received from the `pageKey` field in the previous API response. Filter to retrieve projects from a specific folder by folder ID *** ## Response Array of project objects Unique identifier for the project. Can be a string (for v3 schema) or integer (for v2 schema and earlier) Name of the project Source type of the project **Possible values:** * `script` - Created from text/script * `transcribe` - Created from video/audio transcription * `url` - Created from URL URL to the project preview image or video. May be `null` if no preview is available. ISO 8601 timestamp of when the project was last saved Video aspect ratio of the project **Common values:** * `sixteen-nine` - 16:9 widescreen (landscape) * `nine-sixteen` - 9:16 vertical (portrait) * `onethousandsixhundredandfifty-nine-onethousandandseventy-nine` - Custom aspect ratio Project schema version (e.g., `v2`, `v3`). May be `null` for legacy projects. Type identifier, typically `project` Pagination token for retrieving the next page of results. `null` if there are no more pages. ### Response Examples ```json 200 - Success theme={null} { "items": [ { "id": "20251222191648030d7df02f5b4054d4ca8831f1369459e25", "name": "Hello World Template", "source": "script", "projectPreview": "https://d3uryq9bhgb5qr.cloudfront.net/TeamsAnnual/aa46778d-7965-46e0-967d-b48ee5d6ead9/f1c3b5a7-65f9-4386-a3ef-5ac378c12fa2/image/f1c3b5a7-65f9-4386-a3ef-5ac378c12fa2.jpg", "savedDate": "2025-12-23T01:54:26.755Z", "schemaVersion": "v3", "type": "project" }, { "id": 1762768083800, "name": "my_saved_project", "source": "script", "projectPreview": "https://media.gettyimages.com/id/2161111014/video/create-social-media-content.mp4", "savedDate": "2025-11-10T09:48:03.800Z", "aspectRatio": "sixteen-nine", "schemaVersion": "v2", "type": "project" }, { "id": 1751663566232, "name": "What_is_Calculus", "source": "transcribe", "projectPreview": null, "savedDate": "2025-07-04T21:13:26.094Z", "aspectRatio": "sixteen-nine", "type": "project" } ], "pageKey": null } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v2/projects' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) projects = response.json() # Print project details for project in projects['items']: print(f"Project: {project['name']}") print(f" ID: {project['id']}") print(f" Source: {project['source']}") print(f" Saved: {project['savedDate']}") print("---") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v2/projects', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const data = await response.json(); // Print project details data.items.forEach(project => { console.log(`Project: ${project.name}`); console.log(` ID: ${project.id}`); console.log(` Source: ${project.source}`); console.log(` Saved: ${project.savedDate}`); console.log('---'); }); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type Project struct { ID interface{} `json:"id"` Name string `json:"name"` Source string `json:"source"` ProjectPreview *string `json:"projectPreview"` SavedDate string `json:"savedDate"` AspectRatio string `json:"aspectRatio,omitempty"` SchemaVersion *string `json:"schemaVersion"` Type string `json:"type"` } type ProjectsResponse struct { Items []Project `json:"items"` PageKey *string `json:"pageKey"` } func getProjects(apiKey string) (*ProjectsResponse, error) { url := "https://api.pictory.ai/pictoryapis/v2/projects" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result ProjectsResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { data, err := getProjects("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Total projects: %d\n\n", len(data.Items)) // Print project details for _, project := range data.Items { fmt.Printf("Project: %s\n", project.Name) fmt.Printf(" ID: %v\n", project.ID) fmt.Printf(" Source: %s\n", project.Source) fmt.Printf(" Saved: %s\n", project.SavedDate) fmt.Println("---") } } ``` *** ## Usage Notes Projects are returned in reverse chronological order by saved date, with the most recently saved projects appearing first. **Pagination**: When you have many projects, use the `pageKey` value from the response to retrieve subsequent pages. Pass the `pageKey` as the `pagekey` query parameter in your next request. **Schema Versions**: Projects may have different schema versions (`v2`, `v3`, or `null`). The schema version affects the structure and capabilities of the project. Newer schema versions (v3) typically support more features. *** ## Common Use Cases ### 1. List All Projects Retrieve and display all projects in your account: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": "YOUR_API_KEY"} response = requests.get(url, headers=headers) data = response.json() print(f"Total projects: {len(data['items'])}") for project in data['items']: print(f"{project['name']} ({project['source']})") ``` ### 2. Paginate Through Projects Handle pagination to retrieve all projects across multiple pages: ```python theme={null} def get_all_projects(api_key): """ Retrieve all projects using pagination """ all_projects = [] page_key = None base_url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": api_key} while True: # Build URL with page key if available url = f"{base_url}?pagekey={page_key}" if page_key else base_url response = requests.get(url, headers=headers) data = response.json() # Add projects from this page all_projects.extend(data['items']) # Check if there are more pages page_key = data.get('pageKey') if not page_key: break return all_projects # Get all projects projects = get_all_projects("YOUR_API_KEY") print(f"Retrieved {len(projects)} total projects") ``` ### 3. Filter Projects by Source Type Filter projects based on how they were created: ```javascript theme={null} const getProjectsBySource = async (sourceType) => { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v2/projects', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const data = await response.json(); // Filter by source type const filtered = data.items.filter(p => p.source === sourceType); console.log(`Found ${filtered.length} ${sourceType} projects`); return filtered; }; // Get only script-based projects const scriptProjects = await getProjectsBySource('script'); // Get only transcription-based projects const transcribeProjects = await getProjectsBySource('transcribe'); // Get only URL-based projects const urlProjects = await getProjectsBySource('url'); ``` ### 4. Get Projects from Specific Folder Retrieve projects from a specific folder: ```python theme={null} def get_folder_projects(folder_id, api_key): """ Get all projects within a specific folder """ url = f"https://api.pictory.ai/pictoryapis/v2/projects?folder={folder_id}" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() return data['items'] # Example usage folder_projects = get_folder_projects("your-folder-id", "YOUR_API_KEY") print(f"Folder contains {len(folder_projects)} projects") ``` ### 5. Find Recently Modified Projects Get projects modified within the last N days: ```python theme={null} from datetime import datetime, timedelta import requests def get_recent_projects(days=7, api_key="YOUR_API_KEY"): """ Get projects modified in the last N days """ url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() # Calculate cutoff date cutoff = datetime.now() - timedelta(days=days) # Filter by saved date recent = [ p for p in data['items'] if datetime.fromisoformat(p['savedDate'].replace('Z', '+00:00')) > cutoff ] return recent # Get projects from last 7 days recent_projects = get_recent_projects(days=7) print(f"Found {len(recent_projects)} projects modified in last 7 days") for project in recent_projects: print(f"- {project['name']} (saved: {project['savedDate']})") ``` ### 6. Organize Projects by Schema Version Group projects by their schema version: ```javascript theme={null} const organizeBySchemaVersion = async () => { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v2/projects', { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const data = await response.json(); // Group by schema version const grouped = data.items.reduce((acc, project) => { const version = project.schemaVersion || 'legacy'; if (!acc[version]) acc[version] = []; acc[version].push(project); return acc; }, {}); // Display statistics Object.entries(grouped).forEach(([version, projects]) => { console.log(`${version}: ${projects.length} projects`); }); return grouped; }; // Organize projects const projectsByVersion = await organizeBySchemaVersion(); ``` ### 7. Export Project Metadata Export project metadata to CSV or JSON: ```python theme={null} import json import csv import requests def export_projects_json(api_key, filename="projects.json"): """ Export all projects to JSON file """ url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() with open(filename, 'w') as f: json.dump(data['items'], f, indent=2) print(f"Exported {len(data['items'])} projects to {filename}") def export_projects_csv(api_key, filename="projects.csv"): """ Export all projects to CSV file """ url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() if not data['items']: print("No projects to export") return # Get fieldnames from first project fieldnames = ['id', 'name', 'source', 'savedDate', 'aspectRatio', 'schemaVersion', 'type'] with open(filename, 'w', newline='') as f: writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore') writer.writeheader() writer.writerows(data['items']) print(f"Exported {len(data['items'])} projects to {filename}") # Export to both formats export_projects_json("YOUR_API_KEY") export_projects_csv("YOUR_API_KEY") ``` ### 8. Search Projects by Name Search for projects by name pattern: ```python theme={null} def search_projects_by_name(search_term, api_key): """ Search for projects matching a name pattern """ url = "https://api.pictory.ai/pictoryapis/v2/projects" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() # Case-insensitive search matches = [ p for p in data['items'] if search_term.lower() in p['name'].lower() ] return matches # Example usage results = search_projects_by_name("hello", "YOUR_API_KEY") print(f"Found {len(results)} projects containing 'hello':") for project in results: print(f"- {project['name']} (ID: {project['id']})") ``` # Update Project Source: https://docs.pictory.ai/api-reference/projects/update-project PUT https://api.pictory.ai/pictoryapis/v2/projects/{projectid} Update an existing project using its unique project ID. Requires the complete updated project object in the request body. ## Overview Update an existing project by sending the complete updated project object. This endpoint allows you to modify scene visuals, subtitle text content, scene settings, and other project attributes. The entire project object must be included in the request—partial updates are not supported. **Complete Object Required**: You must send the entire project object in the request body, even if you are only changing a few fields. Omitting fields may result in data loss or project corruption. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v2/projects/{projectid} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the project to update. Can be either a string (for v3 schema projects) or an integer (for v2 schema and earlier projects). Example: `20251222191648030d7df02f5b4054d4ca8831f1369459e25` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Must be set to `application/json` ``` Content-Type: application/json ``` ### Request Body The request body must contain the complete project object retrieved from the [Get Project by ID](/api-reference/projects/get-project-by-id) endpoint. Modify only the fields you need to change while preserving all other fields. **Do not modify project fields if their usage is unclear.** Incorrect modifications can cause unexpected behavior or corrupt your project. If you have questions about specific fields or their intended use, please contact our support team at [support@pictory.ai](mailto:support@pictory.ai) before making changes. *** ## Updatable Video Elements The following sections describe the video element fields that can be safely modified. These structures match the response from the Get Project by ID endpoint. ### Project-Level Fields These top-level fields can be safely updated: ```json theme={null} { "projectName": "My Updated Video", "audioSpeed": 100, "videoVolume": 50 } ``` | Field | Type | Description | | ------------- | ------ | ---------------------------------------------- | | `projectName` | string | Name of the project | | `audioSpeed` | number | Audio playback speed percentage (100 = normal) | | `videoVolume` | number | Background video volume level (0-100) | ### Scene Text (`scenes[].text` and `scenes[].sentence`) Update the display text and narration for each scene: ```json theme={null} { "scenes": [ { "text": "Your new scene text here", "sentence": [ { "text": "Your new ", "decoration": ["decor-bold"] }, { "text": "scene text", "highlight": true, "decoration": ["decor-underline"] }, { "text": " here" } ], "keywords": ["scene text"] } ] } ``` | Field | Type | Description | | ----------------------- | ------- | ------------------------------------------------- | | `text` | string | Plain text version of the scene narration | | `sentence` | array | Array of text segments with formatting | | `sentence[].text` | string | Text content of the segment | | `sentence[].highlight` | boolean | Apply keyword highlighting | | `sentence[].decoration` | array | Text decorations: `decor-bold`, `decor-underline` | | `sentence[].case` | string | Text case: `none`, `uppercase`, `lowercase` | | `keywords` | array | Array of highlighted keyword strings | ### Scene Text Styling (`scenes[].styleData`) Modify text appearance for each scene: ```json theme={null} { "scenes": [ { "styleData": { "fontName": "Arial", "fontDisplayName": "Arial", "fontSize": "42", "fontColor": "rgb(255,255,255)", "fontWeight": 400, "textAlign": "left", "keywordColor": "rgba(141, 208, 229, 1)", "textBackgroundColor": "rgba(0,0,0,0.35)", "textShadowColor": "rgba(0,0,0,0)", "textShadowWidthFr": 0.03, "maxLines": 2, "width": 0.7, "preset": "bottom-left" } } ] } ``` | Field | Type | Description | | --------------------- | ------ | --------------------------------------------------------------- | | `fontName` | string | Font family name | | `fontSize` | string | Font size in pixels | | `fontColor` | string | Text color (RGB format) | | `fontWeight` | number | Font weight (400=normal, 700=bold) | | `textAlign` | string | Alignment: `left`, `center`, `right` | | `keywordColor` | string | Highlight color (RGBA format) | | `textBackgroundColor` | string | Background color (RGBA format) | | `maxLines` | number | Maximum display lines | | `width` | number | Container width (0-1) | | `preset` | string | Position: `bottom-left`, `bottom-center`, `center-center`, etc. | ### Scene Settings (`scenes[].settings`) Control scene playback behavior: ```json theme={null} { "scenes": [ { "settings": { "hideText": false } } ] } ``` | Field | Type | Description | | ---------- | ------- | ----------------------------------------- | | `hideText` | boolean | Hide subtitle/caption text for this scene | ### Background Image (`scenes[].background` with `type: "image"`) Update the background image for a scene: ```json theme={null} { "scenes": [ { "background": { "type": "image", "elementData": { "url": "https://your-image-url.com/image.jpg", "preview_jpg": "https://your-image-url.com/preview.jpg", "thumb": "https://your-image-url.com/thumb.jpg", "thumb_jpg": "https://your-image-url.com/thumb.jpg", "library": "uploads", "libraryItemId": "your-item-id", "duration": 0 }, "styleData": { "kenBurns": "kb-364683-oqt0", "imageZoomPan": true, "width": 0.7, "aspectRatio": 1.78, "preset": "center-center", "colorOverlay": { "hide": false, "bgColor": "rgb(0,37,60)", "opacity": 0.5 } } } } ] } ``` | Field | Type | Description | | ------------------------- | ------- | ------------------------------------------------------ | | `type` | string | Must be `"image"` | | `elementData.url` | string | Full resolution image URL | | `elementData.preview_jpg` | string | Preview image URL | | `elementData.thumb` | string | Thumbnail URL | | `elementData.library` | string | Source: `uploads`, `unsplash`, `story_blocks`, `getty` | | `styleData.imageZoomPan` | boolean | Enable Ken Burns zoom/pan effect | | `styleData.colorOverlay` | object | Color overlay settings | ### Background Video (`scenes[].background` with `type: "video"`) Update the background video for a scene: ```json theme={null} { "scenes": [ { "background": { "type": "video", "elementData": { "url": "https://your-video-url.com/video.mp4", "preview_jpg": "https://your-video-url.com/preview.jpg", "thumb": "https://your-video-url.com/thumb.mp4", "thumb_jpg": "https://your-video-url.com/thumb.jpg", "library": "uploads", "libraryItemId": "your-item-id", "duration": 10, "description": "Video description" }, "styleData": { "imageZoomPan": true }, "settings": { "loopVideo": true } } } ] } ``` | Field | Type | Description | | ---------------------- | ------- | ---------------------------------------------------- | | `type` | string | Must be `"video"` | | `elementData.url` | string | Video file URL | | `elementData.duration` | number | Video duration in seconds | | `elementData.library` | string | Source: `uploads`, `story_blocks`, `getty`, `pexels` | | `settings.loopVideo` | boolean | Loop video playback | ### Solid Color Background (`scenes[].background` with `type: "solid"`) Set a solid color background: ```json theme={null} { "scenes": [ { "background": { "type": "solid", "elementData": { "color": "rgb(69,123,113)" }, "styleData": {} } } ] } ``` ### Video Element Overlay (`scenes[].elements[]` with `type: "video"`) Update video clip overlays within a scene: ```json theme={null} { "scenes": [ { "elements": [ { "elementId": "existing-element-id", "type": "video", "isLogo": false, "hide": false, "settings": { "loopVideo": true, "muteClipAudio": true }, "elementData": { "url": "https://your-video-url.com/clip.mp4", "preview_jpg": "https://your-video-url.com/preview.jpg", "thumb": "https://your-video-url.com/thumb.mp4", "thumb_jpg": "https://your-video-url.com/thumb.jpg", "library": "uploads", "libraryItemId": "your-item-id", "duration": 16 }, "styleData": { "top": 4.89, "left": 3.96, "width": 0.92, "aspectRatio": 1.78, "preset": null, "colorOverlay": { "hide": false, "bgColor": "rgb(110,111,132)", "opacity": 0.3 } } } ] } ] } ``` | Field | Type | Description | | ------------------------ | ------- | --------------------------------------------- | | `elementId` | string | Unique element identifier (preserve existing) | | `type` | string | Must be `"video"` | | `hide` | boolean | Hide this element | | `settings.loopVideo` | boolean | Loop the video clip | | `settings.muteClipAudio` | boolean | Mute the video's audio | | `elementData.url` | string | Video file URL | | `styleData.top` | number | Vertical position (%) | | `styleData.left` | number | Horizontal position (%) | | `styleData.width` | number | Element width (0-1) | | `styleData.colorOverlay` | object | Color overlay settings | ### Text Element Overlay (`scenes[].elements[]` with `type: "text"`) Update text overlay elements within a scene: ```json theme={null} { "scenes": [ { "elements": [ { "elementId": "existing-element-id", "type": "text", "componentName": "main_title", "settings": { "textMode": "writeAnything" }, "elementData": { "sentence": [ { "text": "Your Custom Text", "decoration": ["decor-bold", "decor-underline"] } ] }, "styleData": { "fontName": "Arial", "fontSize": "42", "fontColor": "rgb(255,255,255)", "textAlign": "center", "preset": "center-center", "width": 0.7, "animation": { "textAnimation": [ { "name": "Typewriter", "type": ["entry"], "writingStyle": "character", "direction": "up", "speed": { "value": 1 } }, { "name": "None", "type": ["exit"] } ], "textBgAnimation": [] } } } ] } ] } ``` | Field | Type | Description | | ---------------------- | ------ | --------------------------------------------- | | `elementId` | string | Unique element identifier (preserve existing) | | `type` | string | Must be `"text"` | | `settings.textMode` | string | `useStoryText` or `writeAnything` | | `elementData.sentence` | array | Text segments with formatting | | `styleData.fontName` | string | Font family | | `styleData.fontSize` | string | Font size in pixels | | `styleData.fontColor` | string | Text color (RGB) | | `styleData.animation` | object | Entry/exit animation settings | *** ## Quick Reference: Safe Updates **Field**: `scenes[].background` (type: image) Replace background image with Ken Burns effect and color overlay. ```json theme={null} { "background": { "type": "image", "elementData": { "url": "https://images.unsplash.com/photo-example", "preview_jpg": "https://images.unsplash.com/photo-example", "thumb": "https://images.unsplash.com/photo-example?w=200", "library": "unsplash", "libraryItemId": "mR1CIDduGLc", "duration": 0 }, "styleData": { "kenBurns": "kb-364683-oqt0", "imageZoomPan": true, "aspectRatio": 1.78, "colorOverlay": { "hide": false, "bgColor": "rgb(0,37,60)", "opacity": 0.5 } } } } ``` **Field**: `scenes[].background` (type: video) Replace background with a video clip. ```json theme={null} { "background": { "type": "video", "elementData": { "url": "https://video-url.com/video.mp4", "preview_jpg": "https://video-url.com/preview.jpg", "thumb": "https://video-url.com/thumb.mp4", "library": "story_blocks", "libraryItemId": "120818", "duration": 10 }, "styleData": { "imageZoomPan": true }, "settings": { "loopVideo": true } } } ``` **Fields**: `scenes[].text`, `scenes[].sentence`, `scenes[].keywords` Update display text, formatting, and highlighted keywords. ```json theme={null} { "text": "Sweeping views with year-round sunsets", "sentence": [ { "text": "Sweeping views", "highlight": true, "decoration": ["decor-bold", "decor-underline"] }, { "text": " with year-round " }, { "text": "sunsets", "highlight": true } ], "keywords": ["Sweeping views", "sunsets"] } ``` **Field**: `scenes[].styleData` Update text appearance, fonts, colors, and positioning. ```json theme={null} { "styleData": { "fontName": "Arial", "fontDisplayName": "Arial", "fontSize": "42", "fontColor": "rgb(255,255,255)", "fontWeight": 400, "textAlign": "left", "keywordColor": "rgba(141, 208, 229, 1)", "textBackgroundColor": "rgba(0,0,0,0.35)", "textShadowColor": "rgba(0,0,0,0)", "maxLines": 2, "width": 0.7, "preset": "bottom-left" } } ``` **Field**: `scenes[].settings` Control text visibility for the scene. ```json theme={null} { "settings": { "hideText": false } } ``` **Field**: `scenes[].elements[]` (type: video) Update video clips placed as overlays within scenes. ```json theme={null} { "elements": [{ "type": "video", "isLogo": false, "hide": false, "elementData": { "url": "https://video-url.com/clip.mp4", "preview_jpg": "https://video-url.com/preview.jpg", "library": "story_blocks", "duration": 16 }, "settings": { "loopVideo": true, "muteClipAudio": true }, "styleData": { "top": 4.89, "left": 3.96, "width": 0.92, "aspectRatio": 1.78, "colorOverlay": { "hide": false, "bgColor": "rgb(110,111,132)", "opacity": 0.3 } } }] } ``` **Field**: `scenes[].elements[]` (type: text) Update text overlays and their styling/animations. ```json theme={null} { "elements": [{ "type": "text", "componentName": "main_title", "settings": { "textMode": "writeAnything" }, "elementData": { "sentence": [{ "text": "A Stunning Masterpiece", "decoration": ["decor-underline", "decor-bold"] }] }, "styleData": { "fontName": "Arial", "fontSize": "42", "fontColor": "rgb(255,255,255)", "textAlign": "left", "preset": "bottom-left", "animation": { "textAnimation": [{ "name": "Typewriter", "type": ["entry"], "writingStyle": "character", "speed": { "value": 1 } }] } } }] } ``` **Fields**: `projectName`, `audioSpeed`, `videoVolume` Update basic project configuration. ```json theme={null} { "projectName": "AI Video Creation Guide", "audioSpeed": 100, "videoVolume": 50 } ``` *** ## Fields to Avoid Modifying **Do Not Modify These Fields** unless you fully understand their structure and dependencies: * `sceneId`, `elementId`, `projectId` - Unique identifiers * `partitionKey`, `sortKey` - Database keys * `audio_id`, `voiceOverId`, `audioId` - Audio reference IDs * `assetResponseId` - Asset tracking identifiers * `schemaVersion` - Project schema version * `approximateCreationDateTime` - System timestamps * `createdBy`, `modifiedBy`, `cognito_id` - User identifiers * `audios` - Audio configuration (complex dependencies) * `word_markers`, `time_markers`, `subScenes` - Audio timing data Modifying these fields without proper understanding can cause project corruption or rendering failures. Contact [support@pictory.ai](mailto:support@pictory.ai) if you need guidance. *** ## Response The API returns a JSON response indicating the success or failure of the update operation. **Success Response Fields:** | Field | Type | Description | | --------- | ------- | -------------------------------------------------- | | `success` | boolean | `true` if the update was successful | | `message` | string | Confirmation message | | `project` | object | The updated project object with all current values | **Error Response Fields:** | Field | Type | Description | | --------- | ------- | ----------------------------------------- | | `success` | boolean | `false` if the update failed | | `message` | string | Error description | | `error` | string | Detailed error information (if available) | ## Response Examples ```json 200 - Success theme={null} { "success": true, "message": "Project updated successfully", "project": { "projectName": "Updated Project Name", "audioSpeed": 100, "videoVolume": 50, "scenes": [ { "text": "Updated scene text", "sentence": [ { "text": "Updated scene text", "highlight": true } ], "settings": { "hideText": false }, "background": { "type": "image", "elementData": { "url": "https://new-media-url.com/image.jpg" } } } ] } } ``` ```json 400 - Bad Request theme={null} { "success": false, "message": "Invalid project data", "error": "Missing required fields" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "success": false, "message": "Project not found" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # First, get the current project PROJECT_DATA=$(curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json') # Modify the project data (using jq) UPDATED_DATA=$(echo $PROJECT_DATA | jq '.projectName = "Updated Name"') # Send the update curl --request PUT \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data "$UPDATED_DATA" ``` ```python Python theme={null} import requests project_id = "20251222191648030d7df02f5b4054d4ca8831f1369459e25" api_key = "YOUR_API_KEY" headers = { "Authorization": api_key, "Content-Type": "application/json" } # Step 1: Get the current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) project = response.json() # Step 2: Modify the project project['projectName'] = "Updated Project Name" project['audioSpeed'] = 110 # Step 3: Send the update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=project, headers=headers) result = update_response.json() if result.get('success'): print("Project updated successfully") else: print(f"Update failed: {result.get('message')}") ``` ```javascript JavaScript theme={null} const projectId = '20251222191648030d7df02f5b4054d4ca8831f1369459e25'; const apiKey = 'YOUR_API_KEY'; const headers = { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }; // Step 1: Get the current project const getResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { headers: { ...headers, 'accept': 'application/json' } } ); const project = await getResponse.json(); // Step 2: Modify the project project.projectName = 'Updated Project Name'; project.audioSpeed = 110; // Step 3: Send the update const updateResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'PUT', headers, body: JSON.stringify(project) } ); const result = await updateResponse.json(); if (result.success) { console.log('Project updated successfully'); } else { console.log(`Update failed: ${result.message}`); } ``` *** ## Usage Notes **Always Retrieve Before Update**: Always fetch the current project state using the GET endpoint before making modifications. This ensures you have the complete object with all required fields. **Complete Object Required**: Even if you only want to change one field, you must send the entire project object. Omitting fields will cause them to be removed from the project. **Render After Update**: Updating a project only saves the changes to the project data. To generate a new video with your updates, you must call the [Render Project](/api-reference/videos/render-project) endpoint after the update is complete. **Test Changes First**: When making significant updates, consider testing on a copy of the project first to ensure your modifications do not cause rendering issues. *** ## Common Use Cases ### 1. Update Scene Background Visual Replace the background media for a specific scene: ```python theme={null} import requests def update_scene_background(project_id, scene_index, new_media_url, media_type, api_key): """ Update the background visual for a specific scene media_type: "image" or "video" """ headers = {"Authorization": api_key, "Content-Type": "application/json"} # Get current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) project = response.json() # Update the scene background if scene_index < len(project.get('scenes', [])): background = project['scenes'][scene_index].get('background', {}) background['type'] = media_type background['elementData'] = background.get('elementData', {}) background['elementData']['url'] = new_media_url background['elementData']['preview_jpg'] = new_media_url background['elementData']['library'] = 'uploads' if media_type == 'video': background['settings'] = {'loopVideo': True} project['scenes'][scene_index]['background'] = background print(f"Updated scene {scene_index} background to: {new_media_url}") # Send update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=project, headers=headers) result = update_response.json() if result.get('success'): print("Project updated successfully") return result else: print(f"Update failed: {result.get('message')}") return None else: print(f"Scene index {scene_index} out of range") return None # Example usage update_scene_background( "YOUR_PROJECT_ID", 0, # First scene "https://your-media-url.com/new-image.jpg", "image", "YOUR_API_KEY" ) ``` ### 2. Update Scene Text Modify the text and sentence for one or more scenes: ```javascript theme={null} async function updateSceneText(projectId, sceneUpdates, apiKey) { const headers = { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }; // Get current project const getResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { headers } ); const project = await getResponse.json(); // Update scene text sceneUpdates.forEach(({ sceneIndex, newText, keywords }) => { if (sceneIndex < project.scenes.length) { // Update the text field project.scenes[sceneIndex].text = newText; // Update sentence array with formatting project.scenes[sceneIndex].sentence = [ { text: newText, highlight: false, decoration: ['decor-bold'] } ]; // Update keywords if provided if (keywords) { project.scenes[sceneIndex].keywords = keywords; } console.log(`Updated scene ${sceneIndex} text`); } }); // Send update const updateResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'PUT', headers, body: JSON.stringify(project) } ); const result = await updateResponse.json(); return result; } // Example usage const updates = [ { sceneIndex: 0, newText: 'A Stunning Masterpiece', keywords: ['Stunning Masterpiece'] }, { sceneIndex: 1, newText: 'Sweeping views with year-round sunsets', keywords: ['Sweeping views', 'sunsets'] } ]; const result = await updateSceneText('YOUR_PROJECT_ID', updates, 'YOUR_API_KEY'); console.log('Update result:', result); ``` ### 3. Update Scene Settings and Element Settings Apply settings changes to scenes and their elements: ```python theme={null} def update_scene_settings(project_id, settings_updates, api_key): """ Apply settings to multiple scenes settings_updates: dict with scene indices as keys and settings as values Note: - scenes[].settings only supports: hideText - scenes[].elements[].settings supports: loopVideo, muteClipAudio - scenes[].background.settings supports: loopVideo (for video backgrounds) - scenes[].background.styleData supports: imageZoomPan Example: { 0: {"hideText": True}, 2: {"hideText": False} } """ headers = {"Authorization": api_key, "Content-Type": "application/json"} # Get current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) project = response.json() # Update scene settings (only hideText is supported at scene level) for scene_index, new_settings in settings_updates.items(): if scene_index < len(project.get('scenes', [])): current_settings = project['scenes'][scene_index].get('settings', {}) # Only update hideText at scene level if 'hideText' in new_settings: current_settings['hideText'] = new_settings['hideText'] project['scenes'][scene_index]['settings'] = current_settings print(f"Updated settings for scene {scene_index}") # Send update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=project, headers=headers) return update_response.json() def update_element_settings(project_id, scene_index, element_index, element_settings, api_key): """ Update settings for a specific video element overlay element_settings supports: loopVideo, muteClipAudio """ headers = {"Authorization": api_key, "Content-Type": "application/json"} # Get current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) project = response.json() scenes = project.get('scenes', []) if scene_index < len(scenes): elements = scenes[scene_index].get('elements', []) if element_index < len(elements): current_settings = elements[element_index].get('settings', {}) current_settings.update(element_settings) project['scenes'][scene_index]['elements'][element_index]['settings'] = current_settings print(f"Updated element {element_index} settings in scene {scene_index}") # Send update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=project, headers=headers) return update_response.json() # Example usage - Update scene settings (hideText only) scene_settings = { 0: {"hideText": True}, 2: {"hideText": False} } result = update_scene_settings("YOUR_PROJECT_ID", scene_settings, "YOUR_API_KEY") print(f"Scene settings update successful: {result.get('success')}") # Example usage - Update video element settings (loopVideo, muteClipAudio) result = update_element_settings( "YOUR_PROJECT_ID", scene_index=0, element_index=0, element_settings={"loopVideo": True, "muteClipAudio": True}, api_key="YOUR_API_KEY" ) print(f"Element settings update successful: {result.get('success')}") ``` ### 4. Update Project Metadata Change project name and playback settings: ```javascript theme={null} async function updateProjectMetadata(projectId, metadata, apiKey) { const headers = { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }; // Get current project const getResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { headers } ); const project = await getResponse.json(); // Update metadata if (metadata.name) project.projectName = metadata.name; if (metadata.audioSpeed) project.audioSpeed = metadata.audioSpeed; if (metadata.videoVolume !== undefined) project.videoVolume = metadata.videoVolume; console.log('Updated project metadata:', metadata); // Send update const updateResponse = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}`, { method: 'PUT', headers, body: JSON.stringify(project) } ); return await updateResponse.json(); } // Example usage const metadata = { name: 'My Updated Project', audioSpeed: 105, videoVolume: 80 }; const result = await updateProjectMetadata('YOUR_PROJECT_ID', metadata, 'YOUR_API_KEY'); console.log('Result:', result); ``` ### 5. Replace All Scene Backgrounds Replace backgrounds for all scenes at once: ```python theme={null} def replace_all_backgrounds(project_id, media_items, api_key): """ Replace background media for all scenes media_items: list of dicts with 'url' and 'type' keys Example: [ {"url": "https://example.com/image1.jpg", "type": "image"}, {"url": "https://example.com/video1.mp4", "type": "video"}, ] """ headers = {"Authorization": api_key, "Content-Type": "application/json"} # Get current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) project = response.json() scenes = project.get('scenes', []) if len(media_items) != len(scenes): print(f"Warning: {len(media_items)} items provided for {len(scenes)} scenes") return None # Update all scene backgrounds for i, media in enumerate(media_items): background = scenes[i].get('background', {}) background['type'] = media['type'] background['elementData'] = background.get('elementData', {}) background['elementData']['url'] = media['url'] background['elementData']['preview_jpg'] = media['url'] background['elementData']['library'] = 'uploads' if media['type'] == 'video': background['settings'] = {'loopVideo': True} background['elementData']['duration'] = media.get('duration', 10) scenes[i]['background'] = background print(f"Scene {i}: {media['url']} ({media['type']})") project['scenes'] = scenes # Send update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=project, headers=headers) return update_response.json() # Example usage new_backgrounds = [ {"url": "https://media-url.com/scene1.jpg", "type": "image"}, {"url": "https://media-url.com/scene2.mp4", "type": "video", "duration": 15}, {"url": "https://media-url.com/scene3.jpg", "type": "image"} ] result = replace_all_backgrounds("YOUR_PROJECT_ID", new_backgrounds, "YOUR_API_KEY") if result and result.get('success'): print("All backgrounds updated successfully") ``` ### 6. Safe Update with Validation Implement validation before updating: ```python theme={null} import copy def safe_update_project(project_id, update_function, api_key): """ Safely update project with validation update_function: function that takes project dict and modifies it """ headers = {"Authorization": api_key, "Content-Type": "application/json"} # Get current project get_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" response = requests.get(get_url, headers=headers) if response.status_code != 200: print("Failed to fetch project") return None original_project = response.json() updated_project = copy.deepcopy(original_project) # Apply updates try: update_function(updated_project) except Exception as e: print(f"Error applying updates: {e}") return None # Validate critical fields are present required_fields = ['projectName', 'scenes', 'schemaVersion'] for field in required_fields: if field not in updated_project: print(f"Critical field '{field}' missing after update") return None # Send update put_url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}" update_response = requests.put(put_url, json=updated_project, headers=headers) result = update_response.json() if result.get('success'): print("Project updated successfully") else: print(f"Update failed: {result.get('message')}") return result # Example usage def my_updates(project): """Define your updates here""" project['projectName'] = 'New Name' project['audioSpeed'] = 95 # Hide text for first scene (scenes[].settings.hideText) if project['scenes']: if 'settings' not in project['scenes'][0]: project['scenes'][0]['settings'] = {} project['scenes'][0]['settings']['hideText'] = True # Mute audio for first video element (elements[].settings.muteClipAudio) if project['scenes'] and project['scenes'][0].get('elements'): element = project['scenes'][0]['elements'][0] if element.get('type') == 'video': if 'settings' not in element: element['settings'] = {} element['settings']['muteClipAudio'] = True result = safe_update_project("YOUR_PROJECT_ID", my_updates, "YOUR_API_KEY") ``` *** ## Best Practices ### Safety Guidelines 1. **Always Fetch First**: Get the current project state before making any modifications 2. **Preserve All Fields**: Include all fields in the update request, even if unchanged 3. **Test on Copies**: Test significant changes on duplicate projects first 4. **Validate Before Sending**: Ensure all required fields are present before updating 5. **Keep Backups**: Save the original project state before applying updates ### Common Pitfalls to Avoid * **❌ Partial Updates**: Sending only modified fields will delete other fields * **❌ Missing Required Fields**: Omitting critical fields causes project corruption * **❌ Invalid Values**: Using incorrect data types or formats for fields * **❌ Modifying System Fields**: Changing internal IDs or metadata * **❌ No Validation**: Updating without checking the project state first ### Performance Tips * Batch multiple changes in a single update request * Minimize the frequency of updates during active editing * Cache the project object locally during editing sessions * Only fetch/update when necessary to reduce API calls # Get Smart Layouts Source: https://docs.pictory.ai/api-reference/smartlayouts/get-smart-layouts GET https://api.pictory.ai/pictoryapis/v1/smartlayouts Retrieve all smart layout templates available for video creation ## Overview Fetch all smart layout templates available in your Pictory account. Smart layouts are pre-designed visual templates that define how text, visuals, and other elements are arranged in your video scenes. They provide professional styling with consistent positioning, animations, and visual effects. Use this endpoint to retrieve available smart layouts for video creation, allowing users to select from pre-defined layout options that match their content style. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## What Are Smart Layouts? Smart layouts are visual templates that control: Where subtitles and captions appear on screen How background media is displayed and cropped Entry, exit, and emphasis animations for elements Colors, fonts, and visual effects applied to scenes *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/smartlayouts ``` *** ## Request Parameters ### Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns an object containing an array of smart layout templates. List of available smart layout templates Unique identifier for the smart layout. Use this value for the `smartLayoutId` parameter when creating videos. Descriptive name of the smart layout (e.g., "Modern minimalist", "Kinetic", "Chic") Version of the layout schema (e.g., "v3") ### Response Examples ```json 200 - Success theme={null} { "items": [ { "templateId": "202507141030549677ylq8k8gu44vy1y", "templateName": "Modern minimalist", "schemaVersion": "v3" }, { "templateId": "20250813042630669e658e159aa60455692c1dad5473adhg7", "templateName": "Kinetic", "schemaVersion": "v3" }, { "templateId": "20250813042630669e658e159aa60455692c1dad5473adaa3", "templateName": "Chic", "schemaVersion": "v3" }, { "templateId": "20250813042630669e658e159aa60455692c1dad5473adcf7", "templateName": "Wanderlust", "schemaVersion": "v3" }, { "templateId": "20250813042630669e658e159aa60455692c1dad5473adtd3", "templateName": "Bulletin", "schemaVersion": "v3" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Default Smart Layouts The following smart layouts are available by default: | Layout Name | Style | Best Used For | | --------------------- | ------------------------------------------- | -------------------------------------------------------- | | **Modern minimalist** | Clean, simple design with subtle animations | Professional content, corporate videos, tutorials | | **Kinetic** | Dynamic, energetic with bold movements | Social media content, promotional videos, engaging clips | | **Chic** | Elegant, sophisticated styling | Fashion, lifestyle, premium brand content | | **Wanderlust** | Travel-inspired, adventurous feel | Travel content, adventure videos, outdoor themes | | **Bulletin** | News-style, informative layout | News updates, announcements, educational content | You can also create custom smart layouts in the Pictory App. Custom layouts will appear in your API response alongside the default layouts. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/smartlayouts' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/smartlayouts" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) data = response.json() print(f"Total smart layouts available: {len(data['items'])}\n") # Display all layouts for layout in data['items']: print(f"- {layout['templateName']} (ID: {layout['templateId']})") ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/smartlayouts', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const data = await response.json(); console.log(`Total smart layouts available: ${data.items.length}\n`); // Display all layouts data.items.forEach(layout => { console.log(`- ${layout.templateName} (ID: ${layout.templateId})`); }); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` *** ## Using Smart Layouts in Video Creation Once you have the layout ID, use it with the `smartLayoutId` parameter when creating videos: ### Using Layout ID ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_video', smartLayoutId: '202507141030549677ylq8k8gu44vy1y', // Modern minimalist scenes: [{ story: 'Your video content here', createSceneOnEndOfSentence: true }] }) }); ``` ### Using Layout Name Alternatively, you can use the `smartLayoutName` parameter with the exact template name: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_video', smartLayoutName: 'modern minimalist', // Case-insensitive scenes: [{ story: 'Your video content here', createSceneOnEndOfSentence: true }] }) }); ``` You cannot use both `smartLayoutId` and `smartLayoutName` in the same request. Choose one method to specify your layout. *** ## Error Handling ```json theme={null} { "message": "Unauthorized" } ``` **Solution:** Check your API key is valid and correctly formatted in the Authorization header. ```json theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` **Solution:** Retry the request. If the issue persists, contact support. *** ## Related APIs Use smart layouts when creating video previews Apply smart layouts to rendered videos Customize subtitle styling within layouts Combine layouts with brand settings # Create Template Source: https://docs.pictory.ai/api-reference/templates/create-template POST https://api.pictory.ai/pictoryapis/v1/templates Upload a Pictory project file to create a reusable video template ## Overview Upload a Pictory project file (`.pictai`) downloaded from the Pictory web app to create a reusable video template. Templates allow you to standardize video creation by defining a base structure with customizable variables that can be populated with different content for each video instance. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/templates ``` ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` Must be set to `application/octet-stream` for file uploads ``` Content-Type: application/octet-stream ``` ### Body Parameters The Pictory project file to upload. Must be a `.pictai` file exported from the Pictory web application. **File Requirements:** * Format: `.pictai` (Pictory project file) * Source: Downloaded from Pictory web app * Content: Complete project configuration including scenes, audio, and variables ## Response Unique identifier for the created template Name of the template Language code of the template (e.g., `en` for English) Whether the template is published and available for use Whether the template has been marked as deprecated Audio configuration for the template URL to the background music file Volume level for background music (0-100) AI voice-over configuration settings Array of scene objects that make up the template Unique identifier for the scene Array of subtitle text objects for the scene The subtitle text content Configuration for the scene's background visual (image or video) Template variables that can be customized when creating videos from this template **Common variables:** * `customer_name` - Customer or recipient name * `payment_date` - Payment or transaction date * `loan_account_number` - Account or reference number * `customer_support_number` - Support contact number * `support_email_id` - Support email address Variable names depend on your template design. These are placeholders that will be replaced with actual values when generating videos from the template. ## Response Examples ```json 200 - Success theme={null} { "templateId": "tmpl_abc123xyz", "name": "Pending Loan Payment", "language": "en", "published": true, "deprecated": false, "audio": { "musicUrl": "https://cdn.pictory.ai/music/background-track.mp3", "musicVolume": 50, "aiVoice": { "voiceId": "en-US-Standard-A", "speed": 100 } }, "scenes": [ { "sceneId": "scene_001", "subtitles": [ { "text": "Dear {{customer_name}}, your payment is due on {{payment_date}}" } ], "backgroundVisual": { "type": "video", "url": "https://cdn.pictory.ai/visuals/payment-reminder.mp4" } } ], "variables": { "customer_name": "John Doe", "payment_date": "2024-01-15", "loan_account_number": "LOAN-12345", "customer_support_number": "1-800-555-0123", "support_email_id": "support@example.com" } } ``` ```json 400 - Bad Request theme={null} { "success": false, "message": "Invalid file format. Please upload a .pictai file" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 413 - Payload Too Large theme={null} { "success": false, "message": "File size exceeds maximum allowed limit" } ``` ## Code Examples ```bash cURL theme={null} curl --location 'https://api.pictory.ai/pictoryapis/v1/templates' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/octet-stream' \ --data-binary '@Pending Loan Payment.pictai' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/octet-stream" } # Upload the .pictai file with open("Pending Loan Payment.pictai", "rb") as file: response = requests.post(url, headers=headers, data=file) result = response.json() if response.status_code == 200: template_id = result.get('templateId') print(f"Template created successfully: {template_id}") print(f"Template name: {result.get('name')}") print(f"Variables: {list(result.get('variables', {}).keys())}") else: print(f"Failed to create template: {result.get('message')}") ``` ```javascript JavaScript theme={null} const fs = require('fs'); const url = 'https://api.pictory.ai/pictoryapis/v1/templates'; const filePath = 'Pending Loan Payment.pictai'; // Read the file const fileBuffer = fs.readFileSync(filePath); // Upload template const response = await fetch(url, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/octet-stream' }, body: fileBuffer }); const result = await response.json(); if (response.ok) { console.log(`Template created: ${result.templateId}`); console.log(`Template name: ${result.name}`); console.log(`Variables:`, Object.keys(result.variables || {})); } else { console.error(`Failed to create template: ${result.message}`); } ``` ## Usage Notes **File Source**: The `.pictai` file must be exported from the Pictory web application. You can download project files from your Pictory dashboard. **File Size Limits**: Ensure your template file is within the allowed size limit. Large files with many high-resolution assets may exceed upload limits. **Template Variables**: Design your templates with variables (e.g., `{{customer_name}}`, `{{payment_date}}`) to make them reusable across different video instances. ## How to Export a .pictai File To create a template, you first need to export a project file from the Pictory web app: 1. **Create or Open a Project** in the Pictory web application 2. **Design Your Template** with the desired scenes, text, visuals, and audio 3. **Add Variables** using double curly braces (e.g., `{{variable_name}}`) in text fields 4. **Export the Project** as a `.pictai` file from the project menu 5. **Upload via API** using this endpoint ## Template Variables Variables are placeholders in your template that can be replaced with actual values when creating videos. Use the following format: ### Variable Syntax ``` {{variable_name}} ``` ### Common Use Cases for Variables * **Personalization**: Customer names, account numbers * **Dynamic Dates**: Payment dates, deadlines, event dates * **Contact Information**: Phone numbers, email addresses, support contacts * **Custom Content**: Product names, prices, locations, offers ### Example Template Text ``` Dear {{customer_name}}, Your payment of ${{payment_amount}} is due on {{payment_date}}. Account Number: {{loan_account_number}} For assistance, contact us at {{customer_support_number}} or email {{support_email_id}}. ``` ## Common Use Cases ### 1. Upload Template from File Upload a template file and handle the response: ```python theme={null} import requests def upload_template(file_path, api_key): """ Upload a .pictai file to create a template """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": api_key, "Content-Type": "application/octet-stream" } try: with open(file_path, "rb") as file: response = requests.post(url, headers=headers, data=file) if response.status_code == 200: template = response.json() print(f"✓ Template created: {template['name']}") print(f" Template ID: {template['templateId']}") print(f" Language: {template['language']}") print(f" Scenes: {len(template.get('scenes', []))}") print(f" Variables: {', '.join(template.get('variables', {}).keys())}") return template else: error = response.json() print(f"✗ Upload failed: {error.get('message')}") return None except FileNotFoundError: print(f"✗ File not found: {file_path}") return None except Exception as e: print(f"✗ Error: {str(e)}") return None # Example usage template = upload_template( "Pending Loan Payment.pictai", "YOUR_API_KEY", user_id="user_12345" ) ``` ### 2. Batch Upload Multiple Templates Upload multiple template files at once: ```python theme={null} import os import requests from pathlib import Path def batch_upload_templates(directory, api_key): """ Upload all .pictai files from a directory """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": api_key, "Content-Type": "application/octet-stream" } # Find all .pictai files template_files = list(Path(directory).glob("*.pictai")) if not template_files: print("No .pictai files found in directory") return [] results = [] for file_path in template_files: print(f"Uploading: {file_path.name}") try: with open(file_path, "rb") as file: response = requests.post(url, headers=headers, data=file) if response.status_code == 200: template = response.json() results.append({ 'file': file_path.name, 'status': 'success', 'template_id': template['templateId'], 'name': template['name'] }) print(f" ✓ Success: {template['templateId']}") else: error = response.json() results.append({ 'file': file_path.name, 'status': 'failed', 'error': error.get('message') }) print(f" ✗ Failed: {error.get('message')}") except Exception as e: results.append({ 'file': file_path.name, 'status': 'error', 'error': str(e) }) print(f" ✗ Error: {str(e)}") # Print summary successful = sum(1 for r in results if r['status'] == 'success') print(f"\nSummary: {successful}/{len(results)} templates uploaded successfully") return results # Example usage results = batch_upload_templates( "./templates", "YOUR_API_KEY", user_id="user_12345" ) ``` ### 3. Upload and Extract Template Info Upload a template and extract key information: ```javascript theme={null} const fs = require('fs'); async function uploadAndAnalyzeTemplate(filePath, apiKey) { const url = 'https://api.pictory.ai/pictoryapis/v1/templates'; const headers = { 'Authorization': `${apiKey}`, 'Content-Type': 'application/octet-stream' }; try { // Read and upload file const fileBuffer = fs.readFileSync(filePath); const response = await fetch(url, { method: 'POST', headers, body: fileBuffer }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Upload failed'); } const template = await response.json(); // Extract template information const info = { id: template.templateId, name: template.name, language: template.language, sceneCount: template.scenes?.length || 0, variables: Object.keys(template.variables || {}), hasMusic: Boolean(template.audio?.musicUrl), hasAiVoice: Boolean(template.audio?.aiVoice), published: template.published }; console.log('Template Created:', info); return info; } catch (error) { console.error('Upload failed:', error.message); return null; } } // Example usage const info = await uploadAndAnalyzeTemplate( 'Pending Loan Payment.pictai', 'YOUR_API_KEY', 'user_12345' ); ``` ### 4. Upload with Validation Validate the file before uploading: ```python theme={null} import os import requests from pathlib import Path def validate_and_upload_template(file_path, api_key): """ Validate template file before upload """ # Validation checks if not os.path.exists(file_path): return {'success': False, 'error': 'File does not exist'} if not file_path.endswith('.pictai'): return {'success': False, 'error': 'File must be a .pictai file'} file_size = os.path.getsize(file_path) max_size = 50 * 1024 * 1024 # 50 MB if file_size > max_size: return { 'success': False, 'error': f'File size ({file_size / 1024 / 1024:.1f} MB) exceeds limit' } if file_size == 0: return {'success': False, 'error': 'File is empty'} # Upload url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": api_key, "Content-Type": "application/octet-stream" } print(f"Uploading {Path(file_path).name} ({file_size / 1024:.1f} KB)...") try: with open(file_path, "rb") as file: response = requests.post(url, headers=headers, data=file) if response.status_code == 200: template = response.json() return { 'success': True, 'template': template, 'template_id': template['templateId'] } else: error = response.json() return { 'success': False, 'error': error.get('message', 'Upload failed') } except Exception as e: return {'success': False, 'error': str(e)} # Example usage result = validate_and_upload_template( "template.pictai", "YOUR_API_KEY", user_id="user_12345" ) if result['success']: print(f"Success! Template ID: {result['template_id']}") else: print(f"Error: {result['error']}") ``` ### 5. Upload with Progress Tracking Upload large templates with progress indication: ```python theme={null} import requests from tqdm import tqdm class ProgressFileReader: def __init__(self, file_path): self.file_path = file_path self.file_size = os.path.getsize(file_path) self.file = open(file_path, 'rb') self.progress = tqdm(total=self.file_size, unit='B', unit_scale=True) def read(self, size=-1): data = self.file.read(size) self.progress.update(len(data)) return data def __enter__(self): return self def __exit__(self, *args): self.file.close() self.progress.close() def upload_template_with_progress(file_path, api_key): """ Upload template with progress bar """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": api_key, "Content-Type": "application/octet-stream" } print(f"Uploading {os.path.basename(file_path)}...") with ProgressFileReader(file_path) as file: response = requests.post(url, headers=headers, data=file) if response.status_code == 200: template = response.json() print(f"\n✓ Upload complete: {template['templateId']}") return template else: error = response.json() print(f"\n✗ Upload failed: {error.get('message')}") return None # Example usage template = upload_template_with_progress( "large-template.pictai", "YOUR_API_KEY" ) ``` ## Best Practices ### File Management 1. **Organize Templates**: Keep template files organized by category or purpose 2. **Version Control**: Maintain version history of template files 3. **Naming Convention**: Use descriptive names for template files 4. **Backup**: Keep backups of all template files 5. **Documentation**: Document the purpose and variables of each template ### Template Design 1. **Use Meaningful Variables**: Choose clear, descriptive variable names 2. **Consistent Naming**: Use a consistent naming convention for variables (e.g., `snake_case`) 3. **Default Values**: Provide sensible default values in the template 4. **Test Thoroughly**: Test templates with various variable values before uploading 5. **Optimize Assets**: Keep file sizes reasonable by optimizing images and videos ### Error Handling 1. **Validate Files**: Check file format and size before uploading 2. **Handle Failures**: Implement retry logic for network failures 3. **Log Uploads**: Keep records of uploaded templates 4. **Monitor Limits**: Track upload quotas and limits 5. **Verify Results**: Confirm template creation by checking the response # Delete Template Source: https://docs.pictory.ai/api-reference/templates/delete-template DELETE https://api.pictory.ai/pictoryapis/v1/templates/{templateId} Permanently delete a specific video template using its unique identifier ## Overview Permanently delete a video template from your Pictory account using its unique identifier. This action removes the template and all its configuration data, including scene structures, variables, and settings. **This action is permanent and cannot be undone.** Once deleted, the template and all its configuration will be permanently removed from your account. Videos already created from this template will not be affected. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} DELETE https://api.pictory.ai/pictoryapis/v1/templates/{templateId} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the template to delete Example: `202512230204424435786014d42904bbc95813430789fe736` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Indicates whether the deletion was successful Confirmation message about the deletion *** ## Response Examples ```json 200 - Success theme={null} { "success": true, "message": "Template deleted successfully" } ``` ```json 204 - No Content theme={null} // Empty response body (successful deletion) ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "success": false, "message": "Template not found" } ``` ```json 403 - Forbidden theme={null} { "success": false, "message": "You do not have permission to delete this template" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request DELETE \ --url 'https://api.pictory.ai/pictoryapis/v1/templates/YOUR_TEMPLATE_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests template_id = "202512230204424435786014d42904bbc95813430789fe736" url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.delete(url, headers=headers) if response.status_code in [200, 204]: print(f"Template {template_id} deleted successfully") else: try: result = response.json() print(f"Failed to delete template: {result.get('message', 'Unknown error')}") except: print(f"Failed to delete template: HTTP {response.status_code}") ``` ```javascript JavaScript theme={null} const templateId = '202512230204424435786014d42904bbc95813430789fe736'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`, { method: 'DELETE', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); if (response.status === 200 || response.status === 204) { console.log(`Template ${templateId} deleted successfully`); } else { const result = await response.json().catch(() => ({})); console.log(`Failed to delete template: ${result.message || 'Unknown error'}`); } ``` *** ## Usage Notes **Permanent Deletion**: Deleted templates cannot be recovered. Ensure you have backups of template configurations before deleting. Before deleting a template, consider using the [Get Template By Id](/api-reference/templates/get-template-by-id) endpoint to retrieve and back up the complete template configuration for archival purposes. **Existing Videos**: Videos that were already created from this template will continue to exist and function normally. Only the template itself is deleted. **Permission Requirements**: You can only delete templates that belong to your account or team. Attempting to delete templates from other accounts will result in a 403 Forbidden error. *** ## Common Use Cases ### 1. Delete Single Template with Confirmation Delete a template after user confirmation: ```python theme={null} import requests def delete_template_with_confirmation(template_id, api_key): """ Delete a template after confirming with the user """ # First, get template details get_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = {"Authorization": api_key} template_response = requests.get(get_url, headers=headers) if template_response.status_code != 200: print("Template not found") return False template = template_response.json() template_name = template.get('name', 'Unknown') # Confirm deletion print(f"Are you sure you want to delete template '{template_name}'? (yes/no)") confirmation = input().strip().lower() if confirmation != 'yes': print("Deletion cancelled") return False # Delete template delete_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.status_code in [200, 204]: print(f"Template '{template_name}' deleted successfully") return True else: try: result = delete_response.json() print(f"Failed to delete template: {result.get('message')}") except: print(f"Failed to delete template: HTTP {delete_response.status_code}") return False # Example usage delete_template_with_confirmation("YOUR_TEMPLATE_ID", "YOUR_API_KEY") ``` ### 2. Bulk Delete Deprecated Templates Delete all deprecated templates: ```python theme={null} import requests def delete_deprecated_templates(api_key, dry_run=True): """ Delete all deprecated templates """ headers = {"Authorization": api_key} # Get all templates list_url = "https://api.pictory.ai/pictoryapis/v1/templates" response = requests.get(list_url, headers=headers) templates = response.json().get('items', []) # Find deprecated templates deprecated_templates = [ t for t in templates if t.get('depricated') or t.get('deprecated') ] print(f"Found {len(deprecated_templates)} deprecated templates") if dry_run: print("DRY RUN - Templates that would be deleted:") for template in deprecated_templates: print(f" - {template.get('name', 'Unknown')} (ID: {template['templateId']})") return # Delete deprecated templates deleted_count = 0 for template in deprecated_templates: delete_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template['templateId']}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.status_code in [200, 204]: print(f"✓ Deleted: {template.get('name', 'Unknown')}") deleted_count += 1 else: print(f"✗ Failed: {template.get('name', 'Unknown')}") print(f"\nDeleted {deleted_count} out of {len(deprecated_templates)} deprecated templates") # Example usage - dry run first delete_deprecated_templates("YOUR_API_KEY", dry_run=True) # Then execute for real # delete_deprecated_templates("YOUR_API_KEY", dry_run=False) ``` ### 3. Delete Templates by Language Delete all templates for a specific language: ```javascript theme={null} async function deleteTemplatesByLanguage(language, apiKey, confirmAll = false) { const headers = { 'Authorization': `${apiKey}` }; // Get all templates const listResponse = await fetch( 'https://api.pictory.ai/pictoryapis/v1/templates', { headers } ); const data = await listResponse.json(); // Filter by language const matchingTemplates = data.items.filter(t => t.language === language); console.log(`Found ${matchingTemplates.length} templates with language: ${language}`); if (!confirmAll) { console.log('Set confirmAll=true to delete these templates'); matchingTemplates.forEach(t => { console.log(` - ${t.name} (${t.templateId})`); }); return matchingTemplates; } // Delete matching templates const results = { deleted: [], failed: [] }; for (const template of matchingTemplates) { const deleteResponse = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${template.templateId}`, { method: 'DELETE', headers } ); if (deleteResponse.status === 200 || deleteResponse.status === 204) { results.deleted.push(template.name); console.log(`✓ Deleted: ${template.name}`); } else { results.failed.push(template.name); console.log(`✗ Failed: ${template.name}`); } } console.log(`\nDeleted: ${results.deleted.length}, Failed: ${results.failed.length}`); return results; } // Example usage const results = await deleteTemplatesByLanguage('en', 'YOUR_API_KEY', false); ``` ### 4. Back Up Before Delete Back up template configuration before deletion: ```python theme={null} import requests import json import os from datetime import datetime def backup_and_delete_template(template_id, api_key, backup_dir="./template_backups"): """ Backup template configuration before deleting """ headers = {"Authorization": api_key} # Get template details get_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" response = requests.get(get_url, headers=headers) if response.status_code != 200: print("Template not found") return False template = response.json() template_name = template.get('name', template_id) # Create backup directory os.makedirs(backup_dir, exist_ok=True) # Save template configuration timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{template_name.replace(' ', '_')}_{template_id}_{timestamp}.json" filepath = os.path.join(backup_dir, filename) backup_data = { "deletedDate": datetime.now().isoformat(), "templateData": template } with open(filepath, 'w') as f: json.dump(backup_data, f, indent=2) print(f"Backup saved to: {filepath}") # Delete template print(f"Deleting template...") delete_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.status_code in [200, 204]: print(f"Template '{template_name}' deleted successfully") return True else: print(f"Failed to delete template") return False # Example usage backup_and_delete_template("YOUR_TEMPLATE_ID", "YOUR_API_KEY") ``` ### 5. Safe Delete with Retry Logic Delete template with error handling and retry: ```javascript theme={null} async function safeDeleteTemplate(templateId, apiKey, maxRetries = 3) { const headers = { 'Authorization': `${apiKey}` }; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { console.log(`Attempt ${attempt} to delete template ${templateId}...`); const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`, { method: 'DELETE', headers } ); if (response.status === 401) { throw new Error('Unauthorized - check your API key'); } if (response.status === 404) { console.log('Template already deleted or does not exist'); return { success: true, message: 'Template not found' }; } if (response.status === 200 || response.status === 204) { console.log('Template deleted successfully'); return { success: true }; } const result = await response.json().catch(() => ({})); if (attempt < maxRetries) { console.log(`Deletion failed, retrying in ${attempt * 2} seconds...`); await new Promise(resolve => setTimeout(resolve, attempt * 2000)); } } catch (error) { console.error(`Error on attempt ${attempt}:`, error.message); if (attempt === maxRetries) { throw error; } await new Promise(resolve => setTimeout(resolve, attempt * 2000)); } } throw new Error(`Failed to delete template after ${maxRetries} attempts`); } // Example usage try { const result = await safeDeleteTemplate('YOUR_TEMPLATE_ID', 'YOUR_API_KEY'); console.log('Result:', result); } catch (error) { console.error('Final error:', error.message); } ``` ### 6. Delete Unpublished Draft Templates Delete all unpublished draft templates: ```python theme={null} import requests def delete_unpublished_templates(api_key, confirm=False): """ Delete all unpublished (draft) templates """ headers = {"Authorization": api_key} # Get all templates list_url = "https://api.pictory.ai/pictoryapis/v1/templates" response = requests.get(list_url, headers=headers) templates = response.json().get('items', []) # Find unpublished templates unpublished = [t for t in templates if not t.get('published')] print(f"Found {len(unpublished)} unpublished templates") if not unpublished: print("No unpublished templates to delete") return # List templates for i, template in enumerate(unpublished, 1): print(f"{i}. {template.get('name', 'Unknown')} (ID: {template['templateId']})") if not confirm: print("\nSet confirm=True to delete these templates") return # Delete templates deleted_count = 0 for template in unpublished: delete_url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template['templateId']}" delete_response = requests.delete(delete_url, headers=headers) if delete_response.status_code in [200, 204]: print(f"✓ Deleted: {template.get('name', 'Unknown')}") deleted_count += 1 else: print(f"✗ Failed: {template.get('name', 'Unknown')}") print(f"\nDeleted {deleted_count} out of {len(unpublished)} unpublished templates") # Example usage delete_unpublished_templates("YOUR_API_KEY", confirm=False) ``` *** ## Best Practices ### Safety Measures 1. **Always back up before deletion**: Use the Get Template By Id endpoint to retrieve the complete configuration before deleting 2. **Implement confirmation**: Require user confirmation before executing delete operations 3. **Use dry runs**: Test deletion logic with dry run mode before executing 4. **Log deletions**: Keep audit logs of deleted templates for accountability 5. **Handle errors gracefully**: Implement proper error handling and retry logic ### Common Pitfalls * **❌ No Backup**: Deleting without backing up the template configuration * **❌ Bulk Operations**: Deleting multiple templates without confirmation * **❌ Production Testing**: Testing deletion on production templates * **❌ Ignoring Errors**: Not checking response status codes ### Bulk Operations When deleting multiple templates: * Add delays between deletions to avoid rate limiting * Process deletions in batches * Implement comprehensive error handling * Provide progress updates to users * Log both successes and failures ### Recovery Planning Since deletion is permanent: * Maintain regular backups of template configurations * Export template data before deletion * Keep deletion logs for audit trails * Store template JSON files for potential recreation *** ## Related Endpoints * [Get Templates](/api-reference/templates/get-templates) - List all available templates * [Get Template By Id](/api-reference/templates/get-template-by-id) - Retrieve template details before deleting * [Update Template](/api-reference/templates/update-template) - Modify template instead of deleting * [Create Template](/api-reference/templates/create-template) - Create a new template # Get Template by ID Source: https://docs.pictory.ai/api-reference/templates/get-template-by-id GET https://api.pictory.ai/pictoryapis/v1/templates/{templateId} Retrieve detailed information about a specific video template using its unique identifier ## Overview Retrieve comprehensive details about a specific video template using its unique identifier. This endpoint returns the complete template configuration, including scene structure, variables, voice-over settings, background music, and S3 storage URLs for project files. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/templates/{templateId} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the template to retrieve Example: `202512230204424435786014d42904bbc95813430789fe736` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response The response contains detailed template information including configuration, scenes, variables, and media settings. Unique identifier for the template Name of the template Language code of the template (e.g., `en` for English) Indicates whether the template is published and available for use Indicates whether the template has been deprecated (note: API uses "depricated" spelling) Background music configuration Whether background music is enabled for this template URL to the background music track Music volume level (0.0 to 1.0) Voice-over configuration Whether voice-over is enabled for this template AI voice settings Voice speaker ID Voice speed percentage (100 = normal speed) Voice amplification level Array of scene objects that define the template structure Unique identifier for the scene Subtitle configurations for the scene Subtitle text, may include template variables in `{{variableName}}` format Visual layers for the scene, organized as nested arrays Unique identifier for the layer Layer type (e.g., `text`, `image`, `video`) Text content for text layers Template variables that can be customized when creating videos from this template. Keys are variable names, values are default values or placeholders. Example: `{"Name": "NAME", "Company": "COMPANY_NAME"}` Template schema version (e.g., `v2`, `v3`) S3 URL to the complete project template JSON file S3 URL to the project template structure JSON file *** ## Response Examples ```json 200 - Success theme={null} { "templateId": "202512230204424435786014d42904bbc95813430789fe736", "name": "Hello World Template", "language": "en", "published": true, "depricated": false, "backgroundMusic": { "enabled": true, "musicUrl": "https://tracks.melod.ie/track_versions/71/MEL106_09_1_Raw_Power_%28Full%29_Stefano_Mastronardi_stream.mp3", "volume": 0.1 }, "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "3108", "speed": 100, "amplificationLevel": 0 } ] }, "scenes": [ { "sceneId": "20251222191719950af11290d66804b7f97cad70cde355bcc", "subtitles": [ { "text": "Hi {{Name}}, how are you doing today?" } ], "layers": [ [ { "layerId": "2025122211203035206c45618d2c74013a7022de479df95f2", "type": "text", "text": "Hello" } ] ] } ], "variables": { "Name": "NAME" }, "schemaVersion": "v3", "project": "https://projects-prod.s3.us-east-2.amazonaws.com/project_templates/teams/aa46778d-7965-46e0-967d-b48ee5d6ead9/202512230204424435786014d42904bbc95813430789fe736.json", "structure": "https://projects-prod.s3.us-east-2.amazonaws.com/project_template_structures/teams/aa46778d-7965-46e0-967d-b48ee5d6ead9/202512230204424435786014d42904bbc95813430789fe736.json" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Template not found" } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST", "message": "Invalid template ID" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/templates/YOUR_TEMPLATE_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests template_id = "202512230204424435786014d42904bbc95813430789fe736" url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) template = response.json() # Print template information print(f"Template: {template['name']}") print(f" ID: {template['templateId']}") print(f" Language: {template['language']}") print(f" Published: {template['published']}") print(f" Schema Version: {template['schemaVersion']}") print(f" Variables: {list(template.get('variables', {}).keys())}") print(f" Scenes: {len(template['scenes'])}") ``` ```javascript JavaScript theme={null} const templateId = '202512230204424435786014d42904bbc95813430789fe736'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`, { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const template = await response.json(); // Print template information console.log(`Template: ${template.name}`); console.log(` ID: ${template.templateId}`); console.log(` Language: ${template.language}`); console.log(` Published: ${template.published}`); console.log(` Schema Version: ${template.schemaVersion}`); console.log(` Variables: ${Object.keys(template.variables || {}).join(', ')}`); console.log(` Scenes: ${template.scenes.length}`); ``` *** ## Usage Notes Use this endpoint to retrieve template details before using it to create videos or before updating the template configuration. **Template Variables**: The `variables` object shows all customizable placeholders in the template. These can be replaced with actual values when creating videos from the template. **S3 URLs**: The `project` and `structure` URLs point to S3 storage containing the complete template configuration and structure data. *** ## Common Use Cases ### 1. Retrieve Template Details Get complete information about a specific template: ```python theme={null} import requests def get_template_details(template_id, api_key): """ Retrieve detailed information about a template """ url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) if response.status_code == 200: template = response.json() print(f"Template: {template['name']}") print(f"Language: {template['language']}") print(f"Schema: {template['schemaVersion']}") print(f"\nSettings:") print(f" Background Music: {'Enabled' if template.get('backgroundMusic', {}).get('enabled') else 'Disabled'}") print(f" Voice Over: {'Enabled' if template.get('voiceOver', {}).get('enabled') else 'Disabled'}") print(f"\nStructure:") print(f" Scenes: {len(template['scenes'])}") print(f" Variables: {', '.join(template.get('variables', {}).keys())}") return template else: print(f"Error: {response.status_code}") return None # Example usage template = get_template_details("YOUR_TEMPLATE_ID", "YOUR_API_KEY") ``` ### 2. Extract Template Variables Identify all customizable variables in a template: ```javascript theme={null} async function getTemplateVariables(templateId, apiKey) { const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`, { headers: { 'Authorization': `${apiKey}` } } ); const template = await response.json(); // Extract variables from template const variables = template.variables || {}; console.log(`Template: ${template.name}`); console.log(`\nCustomizable Variables:`); Object.entries(variables).forEach(([key, defaultValue]) => { console.log(` - ${key}: ${defaultValue}`); }); // Also check for variables in subtitles const subtitleVariables = new Set(); template.scenes.forEach(scene => { scene.subtitles?.forEach(subtitle => { const matches = subtitle.text.match(/\{\{(\w+)\}\}/g); if (matches) { matches.forEach(match => { const varName = match.replace(/\{\{|\}\}/g, ''); subtitleVariables.add(varName); }); } }); }); console.log(`\nVariables used in subtitles:`); subtitleVariables.forEach(v => console.log(` - ${v}`)); return { defined: variables, usedInSubtitles: Array.from(subtitleVariables) }; } // Example usage const vars = await getTemplateVariables('YOUR_TEMPLATE_ID', 'YOUR_API_KEY'); ``` ### 3. Analyze Template Structure Analyze and report on template structure: ```python theme={null} import requests def analyze_template_structure(template_id, api_key): """ Analyze template structure and generate report """ url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) template = response.json() # Analyze structure analysis = { 'name': template['name'], 'language': template['language'], 'schema_version': template['schemaVersion'], 'total_scenes': len(template['scenes']), 'has_background_music': template.get('backgroundMusic', {}).get('enabled', False), 'has_voice_over': template.get('voiceOver', {}).get('enabled', False), 'variables': list(template.get('variables', {}).keys()), 'scene_details': [] } # Analyze each scene for i, scene in enumerate(template['scenes'], 1): scene_info = { 'scene_number': i, 'scene_id': scene['sceneId'], 'subtitle_count': len(scene.get('subtitles', [])), 'layer_count': sum(len(layer_group) for layer_group in scene.get('layers', [])) } analysis['scene_details'].append(scene_info) # Print report print(f"=== Template Analysis: {analysis['name']} ===\n") print(f"Language: {analysis['language']}") print(f"Schema: {analysis['schema_version']}") print(f"Total Scenes: {analysis['total_scenes']}") print(f"Background Music: {'Yes' if analysis['has_background_music'] else 'No'}") print(f"Voice Over: {'Yes' if analysis['has_voice_over'] else 'No'}") print(f"Variables: {', '.join(analysis['variables']) if analysis['variables'] else 'None'}") print(f"\nScene Breakdown:") for scene in analysis['scene_details']: print(f" Scene {scene['scene_number']}:") print(f" Subtitles: {scene['subtitle_count']}") print(f" Layers: {scene['layer_count']}") return analysis # Example usage analysis = analyze_template_structure("YOUR_TEMPLATE_ID", "YOUR_API_KEY") ``` ### 4. Compare Template Versions Compare two template versions: ```python theme={null} import requests def compare_templates(template_id_1, template_id_2, api_key): """ Compare two templates and identify differences """ headers = {"Authorization": api_key} url = "https://api.pictory.ai/pictoryapis/v1/templates/" # Fetch both templates template1 = requests.get(f"{url}{template_id_1}", headers=headers).json() template2 = requests.get(f"{url}{template_id_2}", headers=headers).json() differences = [] # Compare basic properties if template1['name'] != template2['name']: differences.append(f"Name: '{template1['name']}' vs '{template2['name']}'") if template1['language'] != template2['language']: differences.append(f"Language: '{template1['language']}' vs '{template2['language']}'") if len(template1['scenes']) != len(template2['scenes']): differences.append(f"Scene count: {len(template1['scenes'])} vs {len(template2['scenes'])}") # Compare variables vars1 = set(template1.get('variables', {}).keys()) vars2 = set(template2.get('variables', {}).keys()) if vars1 != vars2: added = vars2 - vars1 removed = vars1 - vars2 if added: differences.append(f"Variables added: {', '.join(added)}") if removed: differences.append(f"Variables removed: {', '.join(removed)}") # Print comparison print(f"=== Template Comparison ===") print(f"Template 1: {template1['name']} ({template_id_1})") print(f"Template 2: {template2['name']} ({template_id_2})") if differences: print(f"\nDifferences found:") for diff in differences: print(f" - {diff}") else: print("\nNo significant differences found") return differences # Example usage compare_templates("TEMPLATE_ID_1", "TEMPLATE_ID_2", "YOUR_API_KEY") ``` ### 5. Download Template Configuration Download and save complete template configuration: ```javascript theme={null} async function downloadTemplateConfiguration(templateId, apiKey) { const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`, { headers: { 'Authorization': `${apiKey}` } } ); const template = await response.json(); // Create backup object const backup = { exportDate: new Date().toISOString(), templateData: template }; // In Node.js, save to file const fs = require('fs'); const filename = `template_${templateId}_${Date.now()}.json`; fs.writeFileSync(filename, JSON.stringify(backup, null, 2)); console.log(`Template configuration saved to ${filename}`); // Also download project and structure files if needed if (template.project) { console.log(`Project URL: ${template.project}`); } if (template.structure) { console.log(`Structure URL: ${template.structure}`); } return backup; } // Example usage await downloadTemplateConfiguration('YOUR_TEMPLATE_ID', 'YOUR_API_KEY'); ``` ### 6. Validate Template Before Use Validate template configuration before creating videos: ```python theme={null} import requests def validate_template(template_id, api_key): """ Validate template before using it to create videos """ url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = {"Authorization": api_key} try: response = requests.get(url, headers=headers) if response.status_code != 200: return False, f"Failed to fetch template: {response.status_code}" template = response.json() # Validation checks issues = [] # Check if published if not template.get('published'): issues.append("Template is not published") # Check if deprecated if template.get('depricated'): issues.append("Template is deprecated") # Check for scenes if not template.get('scenes'): issues.append("Template has no scenes") # Check for empty variables variables = template.get('variables', {}) if variables and any(not v for v in variables.values()): issues.append("Template has empty variable placeholders") # Report results if issues: print(f"Template validation failed:") for issue in issues: print(f" ✗ {issue}") return False, issues else: print(f"✓ Template '{template['name']}' is valid and ready to use") return True, None except Exception as e: return False, f"Error validating template: {str(e)}" # Example usage is_valid, issues = validate_template("YOUR_TEMPLATE_ID", "YOUR_API_KEY") ``` *** ## Best Practices ### Template Inspection 1. **Retrieve Before Update**: Always fetch the template details before updating to understand the current structure 2. **Validate Configuration**: Check that the template is published and not deprecated before using it 3. **Understand Variables**: Review all variables and their default values before creating videos 4. **Check Schema Version**: Be aware of the schema version as it affects available features ### Performance Tips * Cache template details locally to reduce API calls * Only fetch template details when needed (e.g., before creating videos or updating) * Use the template list endpoint to get basic info, then fetch details only for selected templates * Store frequently used template configurations locally *** ## Related Endpoints * [Get Templates](/api-reference/templates/get-templates) - List all available templates * [Update Template](/api-reference/templates/update-template) - Modify template configuration * [Create Template](/api-reference/templates/create-template) - Create a new template # List Templates Source: https://docs.pictory.ai/api-reference/templates/get-templates GET https://api.pictory.ai/pictoryapis/v1/templates Retrieve a list of all video templates associated with your account ## Overview Retrieve a list of all video templates created and associated with your account. Templates are reusable video project configurations that can be used to quickly create new videos with consistent branding, structure, and variable placeholders. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/templates ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Array of template objects Unique identifier for the template Name of the template Language code of the template (e.g., `en` for English) Indicates whether the template is published and available for use Indicates whether the template has been deprecated and should no longer be used *** ## Response Examples ```json 200 - Success theme={null} { "items": [ { "templateId": "202507141030549677ylq8k8gu44vy1y", "name": "Corporate Presentation Template", "language": "en", "published": true, "deprecated": false }, { "templateId": "202508151145238899abc123def456gh", "name": "Social Media Promo", "language": "en", "published": true, "deprecated": false } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST", "message": "Bad request" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/templates \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) templates = response.json() # Print template information for template in templates.get('items', []): print(f"Template: {template['name']}") print(f" ID: {template['templateId']}") print(f" Language: {template['language']}") print(f" Published: {template['published']}") print(f" Deprecated: {template.get('deprecated', False)}") print() ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/templates', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const data = await response.json(); // Print template information data.items.forEach(template => { console.log(`Template: ${template.name}`); console.log(` ID: ${template.templateId}`); console.log(` Language: ${template.language}`); console.log(` Published: ${template.published}`); console.log(` Deprecated: ${template.deprecated || false}`); console.log(''); }); ``` *** ## Usage Notes Templates are returned in the order they were created. Use the `templateId` to reference specific templates when creating new projects. **Published Status**: Only templates with `published: true` are available for creating new videos. Draft or unpublished templates will have `published: false`. **Deprecated Templates**: Templates marked with `deprecated: true` should not be used for new projects, though they may still be accessible for backward compatibility. *** ## Common Use Cases ### 1. List All Available Templates Retrieve and display all templates: ```python theme={null} import requests def get_all_templates(api_key): """ Retrieve all templates associated with the account """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() templates = data.get('items', []) print(f"Found {len(templates)} templates:") for template in templates: status = "Published" if template['published'] else "Draft" deprecated = " (Deprecated)" if template.get('deprecated') else "" print(f" - {template['name']} ({status}){deprecated}") print(f" ID: {template['templateId']}") return templates else: print(f"Error: {response.status_code}") return [] # Example usage templates = get_all_templates("YOUR_API_KEY") ``` ### 2. Filter Published Templates Get only published, non-deprecated templates: ```javascript theme={null} async function getPublishedTemplates(apiKey) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/templates', { headers: { 'Authorization': `${apiKey}` } } ); const data = await response.json(); // Filter for published, non-deprecated templates const publishedTemplates = data.items.filter( template => template.published && !template.deprecated ); console.log(`Found ${publishedTemplates.length} published templates:`); publishedTemplates.forEach(template => { console.log(` - ${template.name} (${template.templateId})`); }); return publishedTemplates; } // Example usage const templates = await getPublishedTemplates('YOUR_API_KEY'); ``` ### 3. Group Templates by Language Organize templates by language: ```python theme={null} from collections import defaultdict import requests def group_templates_by_language(api_key): """ Group templates by language code """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() templates = data.get('items', []) # Group by language by_language = defaultdict(list) for template in templates: language = template.get('language', 'unknown') by_language[language].append(template) # Print grouped results for language, template_list in by_language.items(): print(f"\n{language.upper()} Templates ({len(template_list)}):") for template in template_list: print(f" - {template['name']}") return by_language # Example usage grouped = group_templates_by_language("YOUR_API_KEY") ``` ### 4. Find Template by Name Search for a specific template by name: ```javascript theme={null} async function findTemplateByName(apiKey, searchName) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/templates', { headers: { 'Authorization': `${apiKey}` } } ); const data = await response.json(); // Case-insensitive search const found = data.items.filter( template => template.name.toLowerCase().includes(searchName.toLowerCase()) ); if (found.length > 0) { console.log(`Found ${found.length} matching template(s):`); found.forEach(template => { console.log(` - ${template.name} (ID: ${template.templateId})`); }); return found; } else { console.log(`No templates found matching: ${searchName}`); return []; } } // Example usage const results = await findTemplateByName('YOUR_API_KEY', 'Corporate'); ``` ### 5. Export Template List Export template list to a file: ```python theme={null} import requests import json from datetime import datetime def export_template_list(api_key, output_file="templates.json"): """ Export all templates to a JSON file """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() # Create export data with metadata export_data = { "exportDate": datetime.now().isoformat(), "totalTemplates": len(data.get('items', [])), "templates": data.get('items', []) } # Write to file with open(output_file, 'w') as f: json.dump(export_data, f, indent=2) print(f"Exported {export_data['totalTemplates']} templates to {output_file}") return export_data else: print(f"Error: {response.status_code}") return None # Example usage export_template_list("YOUR_API_KEY", "my_templates.json") ``` ### 6. Template Status Summary Generate a summary report of template statuses: ```python theme={null} import requests def get_template_summary(api_key): """ Generate a summary of template statuses """ url = "https://api.pictory.ai/pictoryapis/v1/templates" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) data = response.json() templates = data.get('items', []) # Calculate statistics total = len(templates) published = sum(1 for t in templates if t.get('published')) deprecated = sum(1 for t in templates if t.get('deprecated')) active = sum(1 for t in templates if t.get('published') and not t.get('deprecated')) # Print summary print("Template Summary:") print(f" Total Templates: {total}") print(f" Published: {published}") print(f" Deprecated: {deprecated}") print(f" Active (Published & Not Deprecated): {active}") print(f" Draft: {total - published}") # Languages breakdown languages = {} for template in templates: lang = template.get('language', 'unknown') languages[lang] = languages.get(lang, 0) + 1 print("\nTemplates by Language:") for lang, count in sorted(languages.items()): print(f" {lang}: {count}") return { 'total': total, 'published': published, 'deprecated': deprecated, 'active': active, 'languages': languages } # Example usage summary = get_template_summary("YOUR_API_KEY") ``` *** ## Best Practices ### Template Selection 1. **Filter Active Templates**: Always filter for `published: true` and `deprecated: false` when presenting templates to users 2. **Cache Template Lists**: Cache the template list to reduce API calls, refreshing periodically or when needed 3. **Handle Language**: Filter templates by language when building multilingual applications 4. **Sort by Name**: Sort templates alphabetically for better user experience ### Performance Tips * Cache template list responses for 5-10 minutes to reduce API calls * Only fetch template list when needed (e.g., on page load or user action) * Use client-side filtering and sorting after fetching the list once * Monitor for deprecated templates and notify users to update their workflows # Update Template Source: https://docs.pictory.ai/api-reference/templates/update-template PUT https://api.pictory.ai/pictoryapis/v1/templates/{templateId} Update the scene configuration of an existing video template ## Overview Update the scene structure and content of an existing video template. This endpoint allows you to modify the template's scene array, which defines the video's structure, content, and visual elements. **Template Modification**: Updating a template affects all future videos created from it. Existing videos created from this template will not be affected. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v1/templates/{templateId} ``` *** ## Request Parameters ### Path Parameters The unique identifier of the template to update Example: `202512230204424435786014d42904bbc95813430789fe736` ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Body Parameters Array of scene objects that define the template structure. Each scene represents a segment in the video template with its own content, visuals, and timing. The exact structure of scene objects should match the scene format used in Pictory projects. Common fields include: * `text`: The text content for the scene * `duration`: Scene duration in seconds * `image`: Background visual URL or reference * `voiceOver`: Voice-over configuration * `settings`: Scene-specific settings *** ## Response Indicates whether the update was successful Confirmation message about the update The ID of the updated template *** ## Response Examples ```json 200 - Success theme={null} { "success": true, "message": "Template updated successfully", "templateId": "202512230204424435786014d42904bbc95813430789fe736" } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "success": false, "message": "Template not found" } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST", "message": "Invalid scene structure" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Update template with new scenes curl --request PUT \ --url 'https://api.pictory.ai/pictoryapis/v1/templates/YOUR_TEMPLATE_ID' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'accept: application/json' \ --data '{ "scenes": [ { "text": "Introduction scene with updated content", "duration": 5 }, { "text": "Main content scene", "duration": 8 } ] }' | python -m json.tool ``` ```python Python theme={null} import requests template_id = "202512230204424435786014d42904bbc95813430789fe736" url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json", "accept": "application/json" } # Define updated scenes updated_scenes = [ { "text": "Introduction scene with updated content", "duration": 5 }, { "text": "Main content scene", "duration": 8 } ] response = requests.put(url, headers=headers, json={"scenes": updated_scenes}) result = response.json() if result.get('success'): print(f"Template {template_id} updated successfully") else: print(f"Failed to update template: {result.get('message')}") ``` ```javascript JavaScript theme={null} const templateId = '202512230204424435786014d42904bbc95813430789fe736'; const url = `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`; // Define updated scenes const updatedScenes = [ { text: 'Introduction scene with updated content', duration: 5 }, { text: 'Main content scene', duration: 8 } ]; const response = await fetch(url, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json', 'accept': 'application/json' }, body: JSON.stringify({ scenes: updatedScenes }) }); const result = await response.json(); if (result.success) { console.log(`Template ${templateId} updated successfully`); } else { console.log(`Failed to update template: ${result.message}`); } ``` *** ## Usage Notes **Test Before Production**: Always test template updates with non-production templates first to ensure the scene structure is valid and produces the expected results. **Scene Structure**: The scenes array should match the structure used in Pictory projects. Refer to the [Update Project](/api-reference/projects/update-project) endpoint documentation for scene object examples. **Version Control**: Consider keeping backup copies of template configurations before making updates, especially for templates actively used in production. *** ## Common Use Cases ### 1. Update Template Text Content Modify the text content across all scenes in a template: ```python theme={null} import requests def update_template_text(template_id, new_scenes, api_key): """ Update template with new scene content """ url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = { "Authorization": api_key, "Content-Type": "application/json" } payload = {"scenes": new_scenes} response = requests.put(url, headers=headers, json=payload) result = response.json() if result.get('success'): print(f"Template updated successfully") return True else: print(f"Update failed: {result.get('message')}") return False # Example usage new_scenes = [ {"text": "Updated introduction", "duration": 5}, {"text": "Updated main content", "duration": 8}, {"text": "Updated conclusion", "duration": 4} ] update_template_text("YOUR_TEMPLATE_ID", new_scenes, "YOUR_API_KEY") ``` ### 2. Add New Scene to Template Add additional scenes to an existing template: ```javascript theme={null} async function addSceneToTemplate(templateId, newScene, apiKey) { // First, retrieve current template structure // Note: You may need to maintain template data separately // as there's no GET endpoint for individual templates const currentScenes = [ // ... existing scenes ]; // Add new scene currentScenes.push(newScene); const url = `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`; const response = await fetch(url, { method: 'PUT', headers: { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ scenes: currentScenes }) }); const result = await response.json(); if (result.success) { console.log('Scene added successfully'); return true; } else { console.error('Failed to add scene:', result.message); return false; } } // Example usage const newScene = { text: 'New closing scene', duration: 5 }; await addSceneToTemplate('YOUR_TEMPLATE_ID', newScene, 'YOUR_API_KEY'); ``` ### 3. Batch Update Multiple Templates Update multiple templates with similar modifications: ```python theme={null} import requests from typing import List, Dict def batch_update_templates( template_ids: List[str], scene_updates: Dict, api_key: str ): """ Update multiple templates with the same scene modifications """ results = { 'success': [], 'failed': [] } headers = { "Authorization": api_key, "Content-Type": "application/json" } for template_id in template_ids: url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" try: response = requests.put( url, headers=headers, json=scene_updates ) result = response.json() if result.get('success'): results['success'].append(template_id) print(f"✓ Updated template: {template_id}") else: results['failed'].append({ 'id': template_id, 'error': result.get('message') }) print(f"✗ Failed template: {template_id}") except Exception as e: results['failed'].append({ 'id': template_id, 'error': str(e) }) print(f"✗ Error updating {template_id}: {e}") print(f"\nBatch update complete:") print(f" Success: {len(results['success'])}") print(f" Failed: {len(results['failed'])}") return results # Example usage template_ids = [ "template_id_1", "template_id_2", "template_id_3" ] scene_updates = { "scenes": [ {"text": "Standardized intro", "duration": 5}, {"text": "Main content placeholder", "duration": 10} ] } results = batch_update_templates(template_ids, scene_updates, "YOUR_API_KEY") ``` ### 4. Update Template with Validation Validate scene structure before updating: ```python theme={null} import requests def validate_scenes(scenes): """ Validate scene structure before updating """ if not isinstance(scenes, list): return False, "Scenes must be an array" if len(scenes) == 0: return False, "Scenes array cannot be empty" for i, scene in enumerate(scenes): if not isinstance(scene, dict): return False, f"Scene {i} must be an object" # Add more validation as needed if 'text' in scene and not isinstance(scene['text'], str): return False, f"Scene {i} text must be a string" if 'duration' in scene and not isinstance(scene['duration'], (int, float)): return False, f"Scene {i} duration must be a number" return True, "Validation passed" def safe_update_template(template_id, scenes, api_key): """ Update template with validation """ # Validate scenes is_valid, message = validate_scenes(scenes) if not is_valid: print(f"Validation error: {message}") return False # Update template url = f"https://api.pictory.ai/pictoryapis/v1/templates/{template_id}" headers = { "Authorization": api_key, "Content-Type": "application/json" } try: response = requests.put( url, headers=headers, json={"scenes": scenes} ) result = response.json() if result.get('success'): print("Template updated successfully") return True else: print(f"Update failed: {result.get('message')}") return False except Exception as e: print(f"Error: {e}") return False # Example usage scenes = [ {"text": "Validated scene 1", "duration": 5}, {"text": "Validated scene 2", "duration": 8} ] safe_update_template("YOUR_TEMPLATE_ID", scenes, "YOUR_API_KEY") ``` ### 5. Rollback Template Changes Implement a rollback mechanism for template updates: ```javascript theme={null} class TemplateManager { constructor(apiKey) { this.apiKey = apiKey; this.backups = new Map(); } async backupTemplate(templateId, scenes) { // Store backup locally this.backups.set(templateId, { scenes: JSON.parse(JSON.stringify(scenes)), timestamp: new Date().toISOString() }); console.log(`Backup created for template ${templateId}`); } async updateTemplate(templateId, newScenes) { const url = `https://api.pictory.ai/pictoryapis/v1/templates/${templateId}`; try { const response = await fetch(url, { method: 'PUT', headers: { 'Authorization': this.apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ scenes: newScenes }) }); const result = await response.json(); if (result.success) { console.log('Template updated successfully'); return { success: true, result }; } else { console.error('Update failed:', result.message); return { success: false, error: result.message }; } } catch (error) { console.error('Error updating template:', error); return { success: false, error: error.message }; } } async rollbackTemplate(templateId) { const backup = this.backups.get(templateId); if (!backup) { console.error('No backup found for this template'); return false; } console.log(`Rolling back to backup from ${backup.timestamp}`); return await this.updateTemplate(templateId, backup.scenes); } } // Example usage const manager = new TemplateManager('YOUR_API_KEY'); // Backup current state const currentScenes = [/* current scenes */]; await manager.backupTemplate('YOUR_TEMPLATE_ID', currentScenes); // Update template const newScenes = [/* new scenes */]; const result = await manager.updateTemplate('YOUR_TEMPLATE_ID', newScenes); // Rollback if needed if (!result.success) { await manager.rollbackTemplate('YOUR_TEMPLATE_ID'); } ``` *** ## Best Practices ### Safety Guidelines 1. **Backup First**: Always backup the current template configuration before making updates 2. **Validate Structure**: Validate scene structure before sending updates to prevent errors 3. **Test Changes**: Test template updates in a development environment first 4. **Version Control**: Maintain version history of template configurations 5. **Gradual Rollout**: When updating production templates, do so gradually and monitor results ### Common Pitfalls * **❌ No Backup**: Updating without backing up the current configuration * **❌ Invalid Structure**: Sending incorrectly structured scene objects * **❌ Missing Fields**: Omitting required fields in scene objects * **❌ Production Testing**: Testing changes directly on production templates ### Performance Tips * Batch similar updates to reduce API calls * Cache template configurations locally to avoid repeated fetches * Use validation before updates to prevent failed requests * Implement retry logic for transient failures *** ## Related Endpoints * [Get Templates](/api-reference/templates/get-templates) - Retrieve all available templates * [Create Template](/api-reference/templates/create-template) - Create a new template * [Update Project](/api-reference/projects/update-project) - Update project scenes (similar structure) # Generate Highlights from Transcription Job Source: https://docs.pictory.ai/api-reference/transcription/generate-highlights-from-job POST https://api.pictory.ai/pictoryapis/v2/transcription/{jobId}/highlights Generate AI-powered video highlights from a completed transcription job ## Overview Generate concise video highlights from a completed transcription job using AI-powered extraction. This endpoint analyzes the transcript from your transcription job and identifies the most important segments to create a summary of your desired duration. **What you will accomplish:** * Extract key highlights from completed transcription jobs * Generate summaries of specific durations * Receive webhook notifications when processing completes * Create engaging short-form content from long videos You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. This endpoint requires a completed transcription job. First use the [Video Transcription API](/api-reference/transcription/video-transcription) to generate a transcript, then use the returned `jobId` with this endpoint. *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be set to `application/json` ``` Content-Type: application/json ``` *** ## Path Parameters The unique identifier of the transcription job. This is the job ID returned from the [Video Transcription API](/api-reference/transcription/video-transcription). Example: `95333422-8e76-4962-812b-5b6d7276451a` *** ## Body Parameters Target duration for the video summary in seconds. The AI will select highlights that fit within this duration. Example: `30` for a 30-second summary, `60` for a 1-minute summary Webhook URL where the summary results will be posted when processing completes. The webhook will receive a POST request with the summary data. Example: `https://your-domain.com/api/webhooks/highlights` Language code for the transcript content. Supported values: `en` (English), `es` (Spanish), `fr` (French), `de` (German), `it` (Italian), `pt` (Portuguese), `ja` (Japanese), `ko` (Korean), `zh` (Chinese), `ar` (Arabic), `hi` (Hindi), `ru` (Russian), and more. Example: `en` for English, `es` for Spanish *** ## Response Indicates whether the request was successfully queued for processing Contains the job information Unique identifier for the highlights generation job. Use this ID to track the job status via the [Get Job by ID API](/api-reference/jobs/get-transcription-job-by-id). *** ## Response Examples ```json 200 - Success theme={null} { "success": true, "data": { "jobId": "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0" } } ``` ```json 400 - Transcription Not Ready theme={null} { "success": false, "data": { "error_code": "4004", "error_message": "Highlights could not be generated as transcription is still in-progress or not found." } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ### Job Status Response (via Get Job API) While the highlights job is processing: ```json theme={null} { "job_id": "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0", "success": true, "data": { "status": "in-progress" } } ``` When the highlights job completes, you will receive the full result with highlight markers via webhook or by polling the [Get Job by ID API](/api-reference/jobs/get-transcription-job-by-id). *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and `YOUR_JOB_ID` with the transcription job ID. ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/transcription/YOUR_JOB_ID/highlights \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "highlight_duration": 30, "webhook": "https://your-domain.com/api/webhooks/highlights", "language": "en" }' ``` ```python Python theme={null} import requests job_id = "95333422-8e76-4962-812b-5b6d7276451a" # Transcription job ID url = f"https://api.pictory.ai/pictoryapis/v2/transcription/{job_id}/highlights" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "highlight_duration": 30, "language": "en", "webhook": "https://your-domain.com/api/webhooks/highlights" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Highlights Job ID: {data['data']['jobId']}") print("Highlights generation started successfully") else: print(f"Error: {data.get('message', 'Unknown error')}") ``` ```javascript JavaScript theme={null} const jobId = '95333422-8e76-4962-812b-5b6d7276451a'; // Transcription job ID const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/transcription/${jobId}/highlights`, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ highlight_duration: 30, language: 'en', webhook: 'https://your-domain.com/api/webhooks/highlights' }) } ); const data = await response.json(); if (data.success) { console.log(`Highlights Job ID: ${data.data.jobId}`); console.log('Highlights generation started successfully'); } else { console.log(`Error: ${data.message || 'Unknown error'}`); } ``` ```php PHP theme={null} 30, 'language' => 'en', 'webhook' => 'https://your-domain.com/api/webhooks/highlights' ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($response, true); if ($httpCode === 200 && $data['success']) { echo "Highlights Job ID: " . $data['data']['jobId'] . "\n"; echo "Highlights generation started successfully\n"; } else { echo "Error: " . ($data['message'] ?? 'Unknown error') . "\n"; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { jobId := "95333422-8e76-4962-812b-5b6d7276451a" // Transcription job ID url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v2/transcription/%s/highlights", jobId) payload := map[string]interface{}{ "highlight_duration": 30, "language": "en", "webhook": "https://your-domain.com/api/webhooks/highlights", } jsonData, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) if result["success"].(bool) { data := result["data"].(map[string]interface{}) fmt.Printf("Highlights Job ID: %s\n", data["jobId"]) fmt.Println("Highlights generation started successfully") } else { fmt.Printf("Error: %s\n", result["message"]) } } ``` *** ## Usage Notes **Async Processing**: This endpoint processes highlights asynchronously. You will receive a `jobId` immediately, and the actual highlights will be generated in the background. **Webhook Notifications**: Provide a webhook URL to receive the completed highlights automatically when processing finishes. This is the recommended approach rather than polling. **Transcription Must Be Complete**: The transcription job must be fully complete before you can generate highlights. If the transcription is still processing, you will receive a 4004 error. *** ## Best Practices 1. **Wait for Transcription**: Always check that the transcription job status is "completed" before calling this endpoint 2. **Webhook Implementation**: Use webhooks instead of polling for better performance and user experience 3. **Duration Selection**: Choose highlight duration based on your target platform (e.g., 15-30s for TikTok, 60s for YouTube Shorts) 4. **Error Handling**: Implement retry logic for 4004 errors if the transcription is still processing *** ## Related Endpoints * [Video Transcription API](/api-reference/transcription/video-transcription) - Generate the transcription job first * [Generate Highlights from Custom Transcript](/api-reference/transcription/generate-highlights-from-transcript) - Use edited transcript instead * [Get Job by ID](/api-reference/jobs/get-transcription-job-by-id) - Check job status and retrieve results # Generate Highlights from Custom Transcript Source: https://docs.pictory.ai/api-reference/transcription/generate-highlights-from-transcript POST https://api.pictory.ai/pictoryapis/v2/transcription/highlights Generate AI-powered video highlights from an edited or custom transcript ## Overview Generate concise video highlights from your own custom or edited transcript. This endpoint is useful when you have manually edited the transcription output or want to provide your own transcript data directly. The AI analyzes your transcript and identifies the most important segments to create a summary of your desired duration. **What you will accomplish:** * Generate highlights from manually edited transcripts * Use custom transcript data that does not come from the transcription API * Create summaries from transcripts you have corrected or enhanced * Apply highlights to videos with pre-existing transcript data You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. If you have not edited the transcript and want highlights from a transcription job, use the [Generate Highlights from Transcription Job](/api-reference/transcription/generate-highlights-from-job) endpoint instead - it is simpler and requires less data. *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be set to `application/json` ``` Content-Type: application/json ``` *** ## Body Parameters Array of sentence-level transcript segments with word-level timing information. The "Try it now" form cannot properly handle nested array inputs. Please use cURL, Postman, or one of the code examples below to test this endpoint. Each sentence object must contain: * `uid` (string, required): Unique identifier for the sentence * `speakerId` (number, required): Speaker identifier (e.g., `1`) * `words` (array, required): Array of word objects Each word object must contain: * `uid` (string, required): Unique identifier for the word * `word` (string, required): The word text * `start_time` (number, required): Start time in seconds (supports decimals) * `end_time` (number, required): End time in seconds (supports decimals) * `speakerId` (number, required): Speaker identifier * `sentence_index` (number, required): Index of the sentence Example format: ```json theme={null} [ { "uid": "sentence-1", "speakerId": 1, "words": [ { "uid": "word-1", "word": "Important", "start_time": 0.0, "end_time": 0.5, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-2", "word": "content", "start_time": 0.5, "end_time": 1.0, "speakerId": 1, "sentence_index": 0 } ] } ] ``` Target duration for the video summary in seconds. The AI will select highlights that fit within this duration. Example: `30` for a 30-second summary, `60` for a 1-minute summary Total duration of the source video in seconds. This helps the AI understand the full context when generating highlights. Example: `120` for a 2-minute video, `300` for a 5-minute video Webhook URL where the summary results will be posted when processing completes. Example: `https://your-domain.com/api/webhooks/video-summary` Language code for the transcript content. Supported values: `en` (English), `es` (Spanish), `fr` (French), `de` (German), `it` (Italian), `pt` (Portuguese), and more. Example: `en` for English, `es` for Spanish *** ## Response Indicates whether the request was successfully queued for processing Contains the job information Unique identifier for the highlights generation job. Use this ID to track the job status via the [Get Job by ID API](/api-reference/jobs/get-transcription-job-by-id). *** ## Response Examples ```json 200 - Success theme={null} { "success": true, "data": { "jobId": "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0" } } ``` ```json 400 - Bad Request (Validation Error) theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "transcript", "errors": "transcript is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ### Job Status Response (via Get Job API) While the highlights job is processing: ```json theme={null} { "job_id": "bbd75639-c3cb-4add-bf7b-e4e39cffb3b0", "success": true, "data": { "status": "in-progress" } } ``` When the highlights job completes, you will receive the full result with highlight markers via webhook or by polling the [Get Job by ID API](/api-reference/jobs/get-transcription-job-by-id). *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --location 'https://api.pictory.ai/pictoryapis/v2/transcription/highlights' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "transcript": [ { "uid": "sentence-1", "speakerId": 1, "words": [ { "uid": "word-1-1", "word": "Click", "start_time": 0.64, "end_time": 0.96, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-1-2", "word": "the", "start_time": 0.96, "end_time": 1.12, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-1-3", "word": "play", "start_time": 1.12, "end_time": 1.36, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-1-4", "word": "button.", "start_time": 1.36, "end_time": 1.68, "speakerId": 1, "sentence_index": 0 } ] }, { "uid": "sentence-2", "speakerId": 1, "words": [ { "uid": "word-2-1", "word": "This", "start_time": 2.0, "end_time": 2.3, "speakerId": 1, "sentence_index": 1 }, { "uid": "word-2-2", "word": "is", "start_time": 2.3, "end_time": 2.5, "speakerId": 1, "sentence_index": 1 }, { "uid": "word-2-3", "word": "important", "start_time": 2.5, "end_time": 3.0, "speakerId": 1, "sentence_index": 1 }, { "uid": "word-2-4", "word": "content.", "start_time": 3.0, "end_time": 3.5, "speakerId": 1, "sentence_index": 1 } ] } ], "highlight_duration": 10, "duration": 120, "language": "en" }' ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/transcription/highlights" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } # Example edited transcript transcript = [ { "uid": "sentence-1", "speakerId": 1, "words": [ { "uid": "word-1-1", "word": "Click", "start_time": 0.64, "end_time": 0.96, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-1-2", "word": "the", "start_time": 0.96, "end_time": 1.12, "speakerId": 1, "sentence_index": 0 }, { "uid": "word-1-3", "word": "play", "start_time": 1.12, "end_time": 1.36, "speakerId": 1, "sentence_index": 0 } ] } ] payload = { "transcript": transcript, "highlight_duration": 10, "duration": 120, "language": "en", "webhook": "https://your-domain.com/api/webhooks/highlights" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Highlights Job ID: {data['data']['jobId']}") print("Highlights generation started successfully") else: print(f"Error: {data.get('message', 'Unknown error')}") ``` ```javascript JavaScript theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v2/transcription/highlights', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ transcript: [ { uid: 'sentence-1', speakerId: 1, words: [ { uid: 'word-1-1', word: 'Click', start_time: 0.64, end_time: 0.96, speakerId: 1, sentence_index: 0 }, { uid: 'word-1-2', word: 'the', start_time: 0.96, end_time: 1.12, speakerId: 1, sentence_index: 0 } ] } ], highlight_duration: 10, duration: 120, language: 'en', webhook: 'https://your-domain.com/api/webhooks/highlights' }) } ); const data = await response.json(); if (data.success) { console.log(`Highlights Job ID: ${data.data.jobId}`); console.log('Highlights generation started successfully'); } else { console.log(`Error: ${data.message || 'Unknown error'}`); } ``` ```php PHP theme={null} 'sentence-1', 'speakerId' => 1, 'words' => [ [ 'uid' => 'word-1-1', 'word' => 'Click', 'start_time' => 0.64, 'end_time' => 0.96, 'speakerId' => 1, 'sentence_index' => 0 ], [ 'uid' => 'word-1-2', 'word' => 'the', 'start_time' => 0.96, 'end_time' => 1.12, 'speakerId' => 1, 'sentence_index' => 0 ] ] ] ]; $payload = [ 'transcript' => $transcript, 'highlight_duration' => 10, 'duration' => 120, 'language' => 'en', 'webhook' => 'https://your-domain.com/api/webhooks/highlights' ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $data = json_decode($response, true); if ($httpCode === 200 && $data['success']) { echo "Highlights Job ID: " . $data['data']['jobId'] . "\n"; echo "Highlights generation started successfully\n"; } else { echo "Error: " . ($data['message'] ?? 'Unknown error') . "\n"; } ?> ``` *** ## Common Use Cases ### 1. Generate Highlights from Edited Transcript After manually correcting or enhancing a transcript: ```python theme={null} import requests def generate_highlights_from_edited_transcript(edited_transcript, duration, api_key): """ Generate highlights after editing the transcript for accuracy """ url = "https://api.pictory.ai/pictoryapis/v2/transcription/highlights" headers = { "Authorization": api_key, "Content-Type": "application/json" } payload = { "transcript": edited_transcript, "highlight_duration": 30, "duration": duration, "language": "en", "webhook": "https://your-domain.com/webhooks/highlights" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Highlights job created: {data['data']['jobId']}") return data['data']['jobId'] else: print(f"Failed: {data.get('message')}") return None # Use after editing transcript edited_transcript = load_edited_transcript_from_database() job_id = generate_highlights_from_edited_transcript(edited_transcript, 120, "YOUR_API_KEY") ``` ### 2. Use Custom Transcript from External Source When you have transcript data from a third-party service: ```javascript theme={null} async function generateHighlightsFromExternalTranscript(externalTranscript) { // Convert external transcript format to Pictory format const pictoryTranscript = externalTranscript.sentences.map((sentence, index) => ({ uid: `sentence-${index + 1}`, speakerId: sentence.speaker_id || 1, words: sentence.words.map((word, wordIndex) => ({ uid: `word-${index + 1}-${wordIndex + 1}`, word: word.text, start_time: word.start, end_time: word.end, speakerId: sentence.speaker_id || 1, sentence_index: index })) })); const response = await fetch( 'https://api.pictory.ai/pictoryapis/v2/transcription/highlights', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ transcript: pictoryTranscript, highlight_duration: 60, duration: externalTranscript.total_duration, language: 'en' }) } ); const data = await response.json(); return data.success ? data.data.jobId : null; } ``` *** ## Usage Notes **Async Processing**: This endpoint processes highlights asynchronously. You will receive a `jobId` immediately, and the actual highlights will be generated in the background. **Webhook Notifications**: Provide a webhook URL to receive the completed highlights automatically. This is the recommended approach instead of polling. **Transcript Format**: Ensure your transcript segments are in chronological order with accurate start and end times. The AI uses timing information to create seamless highlight clips. **Duration Limits**: The `highlight_duration` should be shorter than your total transcript duration. The AI will select the most important segments that fit within the target duration. *** ## Best Practices ### Transcript Quality 1. **Accurate Timing**: Ensure start and end times are precise for smooth highlight transitions 2. **Complete Sentences**: Structure transcript at natural sentence boundaries for better context 3. **No Overlaps**: Word timings should not overlap 4. **Chronological Order**: Sentences and words must be ordered by time 5. **Unique IDs**: Use unique `uid` values for all sentences and words ### Duration Selection 1. **Platform Optimization**: * TikTok/Reels: 15-30 seconds * Instagram: 30-60 seconds * YouTube Shorts: 60 seconds * LinkedIn: 30-90 seconds 2. **Content Type**: * Product demos: 60-90 seconds * Testimonials: 30-45 seconds * Educational: 45-60 seconds ### Webhook Implementation 1. **Return 200 OK Quickly**: Process the webhook payload asynchronously to avoid timeouts 2. **Implement Retry Logic**: Webhooks may be retried if they fail 3. **Validate Signatures**: Implement webhook signature validation for security 4. **Log All Webhooks**: Keep logs for debugging and audit trails *** ## Related Endpoints * [Generate Highlights from Transcription Job](/api-reference/transcription/generate-highlights-from-job) — Simpler option if you have not edited the transcript * [Video Transcription API](/api-reference/transcription/video-transcription) - Generate the initial transcript * [Get Job by ID](/api-reference/jobs/get-transcription-job-by-id) - Check job status and retrieve results # Video Transcription Source: https://docs.pictory.ai/api-reference/transcription/video-transcription POST https://api.pictory.ai/pictoryapis/v2/transcription Generate accurate transcriptions and subtitles for your videos ## Overview The Video Transcription API generates accurate transcriptions and subtitles for your video or audio files. It supports over 20 languages and can automatically detect speech, convert it to text, and provide timing information for subtitle creation. You can also provide your own transcript or request AI-generated highlights from the transcription. This endpoint processes files asynchronously and returns a job ID that you can use to track the transcription status. Once complete, you will receive the full transcript with precise timing information in formats suitable for SRT, VTT, or JSON subtitle files. ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be set to `application/json` ``` Content-Type: application/json ``` ## Request Parameters ### Option 1: Automatic Transcription (Recommended) Use this option to automatically transcribe your video/audio file. The URL of the video or audio file to transcribe. The file must be accessible via HTTPS. Supported formats: MP4, MOV, AVI, MP3, WAV, M4A The type of media file being transcribed. **Options:** * `video` - Video file with audio track (default) * `audio` - Audio-only file Language code for transcription. Defaults to `en-US` if not specified. See the **Supported Languages** section below for the complete list. Webhook URL where transcription results will be sent via POST request when processing completes. The webhook will receive the complete transcript data including text, word-level timing information, and subtitle formats (SRT, VTT). ### Option 2: Provide Your Own Transcript Use this option when you already have word-level transcript data with timing. The URL of the video or audio file. Array of word-level transcript items with timing information. **Required for Option 2 only.** The "Try it now" form may show this as optional because it combines both options, but this field is required when providing your own transcript. Each transcript item must contain: * `content` (string, required): The word or punctuation text * `type` (string, required): Type of content - `"word"`, `"punctuation"`, `"punctuated_word"`, or `"sentence"` * `start` (number, required): Start time in seconds (supports decimals) * `end` (number, required): End time in seconds (supports decimals) * `speaker` (string, optional): Speaker name (default: `"Speaker 1"`) * `endOfSentence` (boolean, optional): Set to `true` if this is the last word/punctuation of a sentence Example format: ```json theme={null} [ { "content": "Hello", "type": "word", "start": 0.0, "end": 0.5, "speaker": "Speaker 1" }, { "content": "world", "type": "word", "start": 0.5, "end": 1.0, "speaker": "Speaker 1" }, { "content": ".", "type": "punctuation", "start": 1.0, "end": 1.0, "speaker": "Speaker 1", "endOfSentence": true } ] ``` Pre-defined highlight segments (when providing your own transcript). Each highlight segment contains timing information for marking important sections. Media type: `"video"` or `"audio"` Language code (e.g., `"en-US"`, `"es-ES"`, `"fr-FR"`) Webhook URL for job completion notification ## Supported Languages The transcription service supports the following languages: * **en-US** - English (United States) * **en-AU** - English (Australia) * **en-GB** - English (United Kingdom) * **fr-CA** - French (Canada) * **de-DE** - German (Germany) * **it-IT** - Italian (Italy) * **es-ES** - Spanish (Spain) * **ja-JP** - Japanese (Japan) * **ko-KR** - Korean (South Korea) * **ru-RU** - Russian (Russia) * **hi-IN** - Hindi (India) * **ta-IN** - Tamil (India) * **pt-BR** - Portuguese (Brazil) * **zh-CN** - Chinese (Simplified) * **ar-SA** - Arabic (Saudi Arabia) * **nl-NL** - Dutch (Netherlands) * **pl-PL** - Polish (Poland) * **tr-TR** - Turkish (Turkey) * **sv-SE** - Swedish (Sweden) * **da-DK** - Danish (Denmark) ## Response ### Success Response When the transcription request is successfully submitted, a job is created and a job ID is returned: Indicates whether the request was successful Contains the transcription job information Unique identifier for the transcription job. Use this to track the job status and retrieve results via the [Get Transcription Job by ID](/api-reference/jobs/get-transcription-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "data": { "jobId": "cbbc5305-3c1c-46f0-bdde-468e5ecd763f" }, "success": true } ``` ```json 400 - Invalid File URL theme={null} { "data": { "error_code": "4000", "error_message": "transcribe - error - - 'content-length'" }, "success": false } ``` ```json 400 - Bad Request theme={null} { "success": false, "data": { "error_code": "4000", "error_message": "CREATE_TRANSCRIPTION_JOB_FAILED" } } ``` ## Next Steps Once you have the `jobId`, poll the [Get Transcription Job by ID](/api-reference/jobs/get-transcription-job-by-id) endpoint to check the job status and retrieve the transcript when processing is complete. Use a polling interval of 10–30 seconds. ## Code Examples ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/transcription \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "fileUrl": "https://example.com/video.mp4", "mediaType": "video", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription" }' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": "https://example.com/video.mp4", "mediaType": "video", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): job_id = data["data"]["jobId"] print(f"Transcription job started: {job_id}") else: print("Error starting transcription") ``` ```javascript JavaScript theme={null} const axios = require('axios'); const url = 'https://api.pictory.ai/pictoryapis/v2/transcription'; const headers = { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }; const payload = { fileUrl: 'https://example.com/video.mp4', mediaType: 'video', language: 'en-US', webhook: 'https://your-domain.com/webhooks/transcription' }; axios.post(url, payload, { headers }) .then(response => { if (response.data.success) { console.log(`Transcription job started: ${response.data.data.jobId}`); } }) .catch(error => { console.error('Error starting transcription:', error); }); ``` ```php PHP theme={null} 'https://example.com/video.mp4', 'mediaType' => 'video', 'language' => 'en-US', 'webhook' => 'https://your-domain.com/webhooks/transcription' ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); if ($data['success']) { echo "Transcription job started: " . $data['data']['jobId']; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v2/transcription" payload := map[string]interface{}{ "fileUrl": "https://example.com/video.mp4", "mediaType": "video", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription", } jsonData, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) if result["success"].(bool) { data := result["data"].(map[string]interface{}) fmt.Printf("Transcription job started: %s\n", data["jobId"]) } } ``` ## Common Use Cases ### 1. Basic Video Transcription Generate a transcript for a video file in English: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": "https://example.com/presentation.mp4", "mediaType": "video", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Job ID: {data['data']['jobId']}") print("You will receive the transcript at your webhook URL when processing is complete") ``` ### 2. Multi-Language Transcription Transcribe content in different languages: ```python theme={null} import requests def transcribe_video(video_url, language_code, webhook_url): url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": video_url, "mediaType": "video", "language": language_code, "webhook": webhook_url } response = requests.post(url, json=payload, headers=headers) return response.json() # Transcribe videos in different languages videos = [ ("https://example.com/english-video.mp4", "en-US"), ("https://example.com/spanish-video.mp4", "es-ES"), ("https://example.com/japanese-video.mp4", "ja-JP") ] for video_url, language in videos: result = transcribe_video(video_url, language, "https://your-domain.com/webhooks/transcription") if result.get("success"): print(f"Started transcription for {language}: {result['data']['jobId']}") ``` ### 3. Audio File Transcription Transcribe audio-only files like podcasts or interviews: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": "https://example.com/podcast-episode.mp3", "mediaType": "audio", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription" } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Podcast transcription started: {data['data']['jobId']}") ``` ### 4. Transcription with Custom Transcript Provide your own transcript and request highlights: ```python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": "https://example.com/webinar.mp4", "mediaType": "video", "language": "en-US", "webhook": "https://your-domain.com/webhooks/transcription", "transcript": [ {"text": "Welcome to today's webinar on API integration.", "start": 0, "end": 4}, {"text": "We'll cover authentication, endpoints, and best practices.", "start": 4, "end": 9}, {"text": "First, let's discuss API keys and security.", "start": 9, "end": 13} ], "highlight": [ {"duration": 60, "type": "summary"} ] } response = requests.post(url, json=payload, headers=headers) data = response.json() if data.get("success"): print(f"Job ID: {data['data']['jobId']}") print("Custom transcript submitted with highlight request") ``` ### 5. Batch Processing Multiple Videos Process multiple videos and track their job IDs: ```python theme={null} import requests import time def start_transcription(file_url, language): url = "https://api.pictory.ai/pictoryapis/v2/transcription" headers = { "Authorization": "YOUR_API_KEY", "Content-Type": "application/json" } payload = { "fileUrl": file_url, "mediaType": "video", "language": language, "webhook": "https://your-domain.com/webhooks/transcription" } response = requests.post(url, json=payload, headers=headers) return response.json() # List of videos to transcribe videos_to_process = [ {"url": "https://example.com/video1.mp4", "language": "en-US", "title": "Introduction"}, {"url": "https://example.com/video2.mp4", "language": "en-US", "title": "Tutorial Part 1"}, {"url": "https://example.com/video3.mp4", "language": "es-ES", "title": "Spanish Version"} ] job_tracker = [] for video in videos_to_process: result = start_transcription(video["url"], video["language"]) if result.get("success"): job_info = { "title": video["title"], "jobId": result["data"]["jobId"], "language": video["language"] } job_tracker.append(job_info) print(f"Started: {video['title']} - Job ID: {result['data']['jobId']}") time.sleep(1) # Rate limiting else: print(f"Failed to start: {video['title']}") # Save job IDs for tracking print(f"\nTotal jobs started: {len(job_tracker)}") for job in job_tracker: print(f" {job['title']}: {job['jobId']}") ``` ## Best Practices 1. **Webhook Configuration**: Ensure your webhook endpoint is configured to handle POST requests and can process the transcript payload. The webhook should return a 200 status code to acknowledge receipt. 2. **File Accessibility**: Make sure your video/audio files are publicly accessible via HTTPS. The transcription service must be able to download the file from the provided URL. 3. **Language Selection**: Choose the correct language code for best transcription accuracy. Using the wrong language will result in poor quality transcripts. 4. **File Format**: Use common formats like MP4, MP3, or WAV for best results. Ensure audio quality is clear for accurate transcription. 5. **Processing Time**: Transcription is an asynchronous process. Processing time varies based on file length and complexity. Use the webhook to receive results rather than polling. 6. **Custom Transcripts**: When providing custom transcripts, ensure timing information is accurate with start and end times in seconds. This enables proper synchronization. 7. **Rate Limiting**: Implement appropriate delays between batch requests to avoid rate limiting. Process videos sequentially with small delays between API calls. 8. **Error Handling**: Implement proper error handling for failed transcriptions. Check the webhook payload for error messages and retry if necessary. # Update Storyboard Elements Source: https://docs.pictory.ai/api-reference/video-storyboard/update-storyboard-elements PUT https://api.pictory.ai/pictoryapis/v2/video/storyboard/{jobid}/elements Update existing render elements in a storyboard preview before rendering the final video ## Overview The Update Storyboard Elements API allows you to modify existing render elements generated from the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview). Use this endpoint to update background visuals, scene text, or background music URLs before rendering the final video using the [Render from Preview API](/api-reference/videos/render-from-preview). This endpoint only updates **existing** elements in the storyboard. You cannot add new elements — only modify elements that were generated during preview creation. Each element is identified by its unique `id`. The storyboard preview job must be in a `completed` status before you can update its elements. Use the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) API to verify the job status. *** ## When to Use This API | Scenario | Description | | ------------------------------ | ---------------------------------------------- | | **Replace background visuals** | Change a scene's background image or video URL | | **Update scene text** | Modify the text content displayed on a scene | | **Change background music** | Replace the background music track URL | | **Adjust element timing** | Modify `startTime` or `duration` of elements | *** ## Workflow ```mermaid theme={null} sequenceDiagram participant App as Your App participant API as Pictory API App->>API: Create Storyboard Preview API-->>App: jobId App->>API: Poll Job Status (GET /jobs/{jobId}) API-->>App: completed + renderParams with elements App->>App: Identify elements to update by id App->>API: Update Storyboard Elements (PUT /storyboard/{jobId}/elements) API-->>App: 204 No Content App->>API: Render from Preview API-->>App: Final rendered video ``` *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v2/video/storyboard/{jobid}/elements ``` *** ## Path Parameters The job ID returned from the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview). The job must be in `completed` status. *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body The request body is a JSON **array** of element objects. Each object must include the required fields (`id`, `elementType`, `type`, `startTime`, `duration`) along with any properties you want to update. Only elements with matching `id` values in the existing storyboard will be updated. The unique identifier of the element to update. Must match an existing element `id` from the storyboard preview `renderParams.elements` array. Examples: `backgroundElement_20260306230202544742ecd40c041449...`, `bgMusic`, `voiceOver`, `SceneText_202603062302035443dd4953bf12...` The type category of the element. Must match the existing element's `elementType`. **Values:** * `backgroundElement` — Scene background image or video * `audioElement` — Background music or voice-over audio * `SceneText` — Scene text overlay (displayed on scenes) * `layerItem` — Layer items such as title text, avatars, or overlays The media type of the element. Must match or be compatible with the element. **Values:** * `video` — Video media * `image` — Image media * `audio` — Audio media * `text` — Text content The start time of the element in seconds. Must be >= 0. The duration of the element in seconds. Must be > 0. *** ## Element Attributes Reference Any additional properties included in the element update object will be merged with the existing element. Below is a comprehensive reference of all attributes for each element type. Element IDs are auto-generated unique strings (e.g., `backgroundElement_20260306230202544742ecd40c041449dac21bb737c799201`). Always retrieve the actual IDs from the preview job response rather than constructing them manually. ### Element Types Summary | Element | `elementType` | `type` | `id` Pattern | Description | | ---------------- | ------------------- | ---------------- | ------------------------------ | -------------------------------------- | | Scene Background | `backgroundElement` | `video`, `image` | `backgroundElement_{uniqueId}` | Background visual for each scene | | Scene Text | `SceneText` | `text` | `SceneText_{uniqueId}` | Sentence-level text overlay on a scene | | Title/Layer Text | `layerItem` | `text` | `{uniqueId}` | Title text, headings, or overlay text | | Layer Visual | `layerItem` | `video`, `image` | `{uniqueId}` | Overlay images, videos, or avatars | | Background Music | `audioElement` | `audio` | `bgMusic` | Background music track | | Voice-Over | `audioElement` | `audio` | `voiceOver` | AI-generated narration audio | *** ### Background Element (`backgroundElement`) The background element is the full-screen visual behind each scene. One background element exists per scene. #### Media Properties | Property | Type | Description | Example | | ----------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `url` | string | Full-resolution media URL used for final video rendering | `"https://cdn.com/video-hd.mp4"` | | `visualUrl` | string | Media URL used for preview playback. Can be a lower-resolution video or even a thumbnail image for faster preview loading. When rendering the final video, `url` is used instead | `"https://cdn.com/video-preview.mp4"` or `"https://cdn.com/preview-thumb.jpg"` | | `type` | string | Media type of `url`: `video` or `image` | `"video"` | | `visualType` | string | Media type of `visualUrl`. Can differ from `type` — e.g., `visualType` may be `image` (thumbnail) while `type` is `video` (full render) | `"image"` | | `backgroundColor` | string | Solid background color in RGBA format. Used as fallback or when no media URL is set | `"rgba(0, 0, 0, 1)"` | #### Display Properties | Property | Type | Default | Description | | ------------ | ------ | ----------- | ---------------------------------------------------------------------------------------------------- | | `width` | string | `"100%"` | Element width as a percentage of the video frame | | `xPercent` | string | `"0%"` | Horizontal position offset as percentage | | `yPercent` | string | `"0%"` | Vertical position offset as percentage | | `objectMode` | string | `"cover"` | How the media fits the frame. `cover` fills and crops to fit, `fit` letterboxes to show entire media | | `cursor` | string | `"default"` | Mouse cursor style in the editor | #### Video Playback Properties | Property | Type | Default | Description | | ---------- | ------- | ------- | --------------------------------------------------------------------------- | | `loop` | boolean | `true` | Whether to loop video playback when it reaches the end | | `mute` | boolean | `true` | Whether to mute the video's built-in audio track | | `segments` | array | `[]` | Time segments for video clipping. Each segment: `{ "start": 0, "end": 10 }` | #### Visual Effects | Property | Type | Description | Example | | ---------------------- | ------- | ------------------------------------------------------- | ------------------ | | `colorOverlay` | object | Semi-transparent color overlay on top of the background | See below | | `colorOverlay.hide` | boolean | Whether the overlay is hidden | `false` | | `colorOverlay.bgColor` | string | Overlay color in RGB format | `"rgb(22,91,110)"` | | `colorOverlay.opacity` | number | Overlay opacity from 0 (transparent) to 1 (opaque) | `0.65` | #### Metadata | Property | Type | Description | Example | | --------- | ------ | ------------------------------------------------ | --------- | | `library` | string | Source library where the media was selected from | `"getty"` | ```json theme={null} { "id": "backgroundElement_20260306230202544742ecd40c041449dac21bb737c799201", "elementType": "backgroundElement", "type": "video", "url": "https://media.gettyimages.com/id/2262113992/video/professor-lectures.mp4", "backgroundColor": "rgba(0, 0, 0, 1)", "xPercent": "0%", "yPercent": "0%", "width": "100%", "objectMode": "cover", "loop": true, "mute": true, "segments": [], "startTime": 0, "duration": 7.6, "colorOverlay": { "hide": false, "bgColor": "rgb(22,91,110)", "opacity": 0.65 }, "cursor": "default", "visualUrl": "https://media.gettyimages.com/id/2262113992/video/professor-lectures.mp4", "visualType": "video", "library": "getty" } ``` *** ### Scene Text (`SceneText`) and Layer Text (`layerItem` with `type: "text"`) Text elements display text content on top of scenes. **SceneText** elements represent the per-sentence text overlay for a scene, while **layerItem** text elements represent titles, headings, or additional text overlays. #### Content Properties | Property | Type | Description | Example | | -------- | ------ | -------------------------------------------------------------------------------------------- | -------------------------------------- | | `text` | string | Text content to display. Supports `` tags to highlight keywords using `keywordColor` | `"AI will save time"` | #### Font & Styling Properties | Property | Type | Default | Description | Example | | -------------- | ------ | --------- | --------------------------------------------------------------------------------------------------------------- | -------------------------- | | `fontFamily` | string | `"Arial"` | Font family name | `"Space Grotesk"` | | `fontWeight` | number | `400` | Font weight (400 = normal, 700 = bold) | `400` | | `fontSize` | string | `"16"` | Font size in pixels | `"32"` | | `fontColor` | string | — | Text color in RGB or RGBA format | `"rgb(255,255,255)"` | | `keywordColor` | string | — | Color applied to text wrapped in `` tags | `"rgba(248, 173, 151, 1)"` | | `textAlign` | string | `"left"` | Text alignment: `left`, `center`, or `right` | `"center"` | | `decoration` | array | `[]` | Text decorations. Values: `"decor-bold"`, `"decor-italics"`, `"decor-underline"`, `"decor-linethrough"` | `["decor-bold"]` | | `case` | string | `"none"` | Text case transformation: `none`, `case-uppercase`, `case-lowercase`, `case-capitalize`, `case-smallcapitalize` | `"none"` | | `lineHeight` | number | `1.3` | Line height multiplier for spacing between lines | `1.48` | #### Text Background Properties | Property | Type | Default | Description | Example | | ---------------------- | ------- | ----------------- | ----------------------------------------------------------------------------------------- | ------------------------ | | `textBackgroundColor` | string | `"rgba(0,0,0,0)"` | Background color behind the text or as a fill for the full-width box | `"rgba(22, 91, 110, 1)"` | | `textBackgroundRadius` | array | `[0,0,0,0]` | Border radius for the text background box: `[topLeft, topRight, bottomRight, bottomLeft]` | `[8, 8, 8, 8]` | | `boxBackground` | boolean | `false` | Whether text has a visible background box | `false` | #### Text Shadow Properties | Property | Type | Default | Description | Example | | ----------------- | ------ | ----------------- | -------------------------------- | ----------------- | | `dropShadowColor` | string | `"rgba(0,0,0,0)"` | Drop shadow color in RGBA format | `"rgba(0,0,0,1)"` | | `dropShadowBlur` | number | `0` | Shadow blur radius | `3` | #### Layout & Position Properties | Property | Type | Default | Description | Example | | -------------------- | ------- | ------- | ----------------------------------------------------------------- | --------------- | | `width` | string | `"90%"` | Text box width as percentage of the video frame | `"70.00%"` | | `fullWidth` | boolean | `false` | Whether text spans the full viewport width with padding | `false` | | `preset` | string | — | Position preset anchor. See [Position Presets](#position-presets) | `"bottom-left"` | | `opacity` | number | `1` | Element transparency from 0 (invisible) to 1 (opaque) | `1` | | `draggable` | boolean | `true` | Whether the element can be repositioned in the editor | `true` | | `resizable` | boolean | `false` | Whether the element can be resized in the editor | `false` | | `canChangeWidthOnly` | boolean | `true` | Restrict resizing to width only (height adjusts automatically) | `true` | | `snapTo` | boolean | `true` | Snap to alignment guidelines when dragging | `true` | | `midHandle` | boolean | `false` | Use center handle for resizing | `false` | | `cropable` | boolean | `false` | Whether the element can be cropped | `false` | #### Bullet Properties | Property | Type | Default | Description | Example | | ------------- | ------- | ----------- | -------------------------------------------- | ----------- | | `bullet` | boolean | `false` | Display a bullet point beside each text line | `false` | | `bulletColor` | string | `"#e30022"` | Bullet point color | `"#e30022"` | | `bulletWidth` | number | `15` | Bullet point width in pixels | `15` | #### Animation Properties | Property | Type | Description | Example | | -------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------ | ---------------- | | `animation` | array | Array of animation objects for entry and exit effects | See below | | `animation[].name` | string | Animation name: `Blur`, `Drift`, `Fade`, `Wipe`, `Show`, `Elastic`, `Typewriter`, `TextReveal`, `Bulletin`, `None` | `"Blur"` | | `animation[].type` | array | Animation phase: `["entry"]` or `["exit"]` | `["entry"]` | | `animation[].writingStyle` | string | How text animates in: `character` (per letter), `word` (per word), `line` (per line), `paragraph` (all at once) | `"paragraph"` | | `animation[].speed` | object | Animation speed configuration | `{ "value": 2 }` | | `animation[].speed.value` | number | Speed value (higher = faster) | `2` | | `animation[].direction` | string | Direction of animation: `up`, `down`, `left`, `right` | `"up"` | #### Computed Properties (read-only) | Property | Type | Description | | ----------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `textLines` | array | Pre-calculated line break and character position data used by the rendering engine. Each entry contains character positions (`charX`), highlight flags (`h`), and line dimensions (`ht`). This is auto-generated — **when updating text content, you must set `textLines` to an empty array `[]` so that line breaks and character positions are recalculated during preview and render** | ```json theme={null} { "id": "SceneText_202603062302035443dd4953bf1294b4283d4b8ba0259fd1a", "elementType": "SceneText", "type": "text", "text": "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency.", "fontFamily": "Space Grotesk", "fontWeight": 400, "fontSize": "32", "fontColor": "rgb(255,255,255)", "keywordColor": "rgba(248, 173, 151, 1)", "textBackgroundColor": "rgba(22, 91, 110, 1)", "dropShadowColor": "rgba(0,0,0,0)", "dropShadowBlur": 3, "textAlign": "left", "decoration": [], "case": "none", "lineHeight": 1.465625, "textBackgroundRadius": [0, 0, 0, 0], "boxBackground": false, "width": "70.00%", "preset": "bottom-left", "opacity": 1, "draggable": true, "resizable": false, "snapTo": true, "midHandle": false, "canChangeWidthOnly": true, "cropable": false, "startTime": 7.6, "duration": 11.1, "animation": [ { "writingStyle": "line", "name": "Drift", "type": ["entry"], "speed": { "value": 1 }, "direction": "left" }, { "name": "None", "type": ["exit"] } ] } ``` ```json theme={null} { "id": "2026030623020497002befca37e444fb880a281abadc0c756", "elementType": "layerItem", "type": "text", "text": "AI's Impact on Educators and Creators", "fontFamily": "Space Grotesk", "fontWeight": 400, "fontSize": "48", "fontColor": "rgb(255,255,255)", "keywordColor": "rgba(248, 173, 151, 1)", "textBackgroundColor": "rgba(0,0,0,0)", "dropShadowColor": "rgba(0,0,0,0)", "dropShadowBlur": 3, "textAlign": "center", "decoration": [], "case": "none", "lineHeight": 1.48125, "textBackgroundRadius": [0, 0, 0, 0], "boxBackground": false, "width": "90.00%", "fullWidth": false, "preset": "center-center", "opacity": 1, "draggable": true, "resizable": false, "snapTo": true, "midHandle": false, "canChangeWidthOnly": true, "cropable": false, "startTime": 0, "duration": 7.6, "animation": [ { "writingStyle": "paragraph", "name": "Blur", "type": ["entry"], "speed": { "value": 2 }, "direction": "up" }, { "name": "None", "type": ["exit"] } ] } ``` *** ### Audio Element (`audioElement`) Audio elements represent background music (`bgMusic`) or voice-over narration (`voiceOver`). #### Audio Properties | Property | Type | Description | Example | | -------- | ------- | --------------------------------------------------------------------------------------- | ----------------------------------------- | | `url` | string | Audio file URL (MP3 or compatible format) | `"https://tracks.melod.ie/.../track.mp3"` | | `fade` | boolean | Whether to apply fade in/out effect to the audio. Typically `true` for background music | `true` | #### Segments | Property | Type | Description | Example | | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | | `segments` | array | Array of audio segment objects. Each segment defines a playback range with its own volume level. Segments correspond to individual scenes in the video | See below | | `segments[].startTime` | number | Start time of the segment in the video timeline (seconds) | `0` | | `segments[].duration` | number | Duration of the segment (seconds) | `7.6` | | `segments[].volume` | number | Volume level for this segment (0 to 1). Background music typically uses `0.1`, voice-over uses `1` | `0.1` | | `segments[].audioTime` | number | Offset position in the audio file to start playback from (seconds) | `0` | ```json theme={null} { "type": "audio", "id": "bgMusic", "elementType": "audioElement", "fade": true, "url": "https://tracks.melod.ie/track_versions/23517/MEL565_15_1_Utopia.mp3", "segments": [ { "startTime": 0, "duration": 7.6, "volume": 0.1, "audioTime": 0 }, { "startTime": 7.6, "duration": 11.1, "volume": 0.1, "audioTime": 7.6 }, { "startTime": 18.7, "duration": 7.6, "volume": 0.1, "audioTime": 18.7 }, { "startTime": 26.3, "duration": 11, "volume": 0.1, "audioTime": 26.3 }, { "startTime": 37.3, "duration": 8.5, "volume": 0.1, "audioTime": 37.3 }, { "startTime": 45.8, "duration": 9.1, "volume": 0.1, "audioTime": 45.8 }, { "startTime": 54.9, "duration": 10.9, "volume": 0.1, "audioTime": 54.9 } ], "startTime": 0, "duration": 100000 } ``` ```json theme={null} { "type": "audio", "id": "voiceOver", "elementType": "audioElement", "url": "https://audios-prod.pictorycontent.com/polly/production/projects/.../voiceover.mp3", "segments": [ { "startTime": 0, "duration": 7.6, "volume": 1, "audioTime": 0 }, { "startTime": 7.6, "duration": 11.1, "volume": 1, "audioTime": 7.6 }, { "startTime": 18.7, "duration": 7.6, "volume": 1, "audioTime": 18.7 }, { "startTime": 26.3, "duration": 11, "volume": 1, "audioTime": 26.3 }, { "startTime": 37.3, "duration": 8.5, "volume": 1, "audioTime": 37.3 }, { "startTime": 45.8, "duration": 9.1, "volume": 1, "audioTime": 45.8 }, { "startTime": 54.9, "duration": 10.9, "volume": 1, "audioTime": 54.9 } ], "startTime": 0, "duration": 100000 } ``` *** ### Layer Visual Element (`layerItem` with `type: "video"` or `"image"`) Layer visual elements are overlay images or videos displayed on top of the background, such as avatars, logos, or decorative graphics. | Property | Type | Default | Description | Example | | ---------------- | ------- | ------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `url` | string | — | Full-resolution media URL used for final video rendering | `"https://cdn.com/avatar-hd.mp4"` | | `visualUrl` | string | — | Media URL used for preview playback. Can be a lower-resolution video or a thumbnail image for faster preview loading | `"https://cdn.com/avatar-preview.mp4"` or `"https://cdn.com/avatar-thumb.jpg"` | | `type` | string | — | Media type of `url`: `video` or `image` | `"video"` | | `visualType` | string | — | Media type of `visualUrl`. Can differ from `type` (e.g., `image` thumbnail for a `video` render) | `"image"` | | `objectMode` | string | `"cover"` | How media fits: `cover` (fill and crop) or `fit` (letterbox) | `"cover"` | | `width` | string | — | Element width as percentage | `"25%"` | | `height` | string | — | Element height as percentage (optional) | `"30%"` | | `preset` | string | — | Position preset anchor | `"bottom-right"` | | `xPercent` | string | — | Horizontal position when no preset | `"70%"` | | `yPercent` | string | — | Vertical position when no preset | `"60%"` | | `opacity` | number | `1` | Transparency (0 to 1) | `1` | | `loop` | boolean | `true` | Loop video playback | `true` | | `mute` | boolean | `false` | Mute video audio | `true` | | `aspectRatio` | number | — | Width/height ratio | `0.5625` | | `flipHorizontal` | boolean | `false` | Mirror the element horizontally | `false` | | `flipVertical` | boolean | `false` | Mirror the element vertically | `false` | | `maskShape` | string | `"rectangle"` | Mask shape: `rectangle` or `circle` | `"circle"` | | `library` | string | — | Source library (e.g., `avatar`, `giphy`) | `"avatar"` | *** ### Position Presets The `preset` property defines a named anchor position for text and layer elements within the video frame: | Preset | Description | | ------------------- | ------------------------------- | | `top-left` | Anchored to top-left corner | | `top-center` | Anchored to top-center | | `top-right` | Anchored to top-right corner | | `center-left` | Anchored to middle-left | | `center-center` | Centered in the frame | | `center-right` | Anchored to middle-right | | `bottom-left` | Anchored to bottom-left corner | | `bottom-center` | Anchored to bottom-center | | `bottom-right` | Anchored to bottom-right corner | | `full-width-top` | Full width, anchored to top | | `full-width-center` | Full width, centered vertically | | `full-width-bottom` | Full width, anchored to bottom | *** ### Text Animations Text elements support entry and exit animations. The `animation` array contains animation configuration objects: | Animation Name | Description | | -------------- | ----------------------------------------- | | `Blur` | Text fades in with a blur effect | | `Drift` | Text slides in from a specified direction | | `Fade` | Simple fade in/out | | `Wipe` | Text wipes in from a direction | | `Show` | Instant appearance | | `Elastic` | Bouncy elastic entrance | | `Typewriter` | Characters appear one by one | | `TextReveal` | Text reveals progressively | | `Bulletin` | News bulletin style animation | | `None` | No animation | **Writing Styles** control the granularity of the animation: | Writing Style | Description | | ------------- | ------------------------------------- | | `character` | Animate one character at a time | | `word` | Animate one word at a time | | `line` | Animate one line at a time | | `paragraph` | Animate the entire text block at once | *** ## Response ```json 204 - Success theme={null} // No content returned. The elements have been updated successfully. ``` ```json 400 - Bad Request theme={null} { "success": false, "error": { "code": "INVALID_REQUEST", "message": "Validation error details" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` A `204 No Content` response indicates the elements were updated successfully. The updated elements will be reflected when you: * Retrieve the job details using the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) API * Render the video using [Render from Preview](/api-reference/videos/render-from-preview) *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and `YOUR_JOB_ID` with the storyboard preview job ID. ### Update Background Visual ```bash cURL theme={null} curl --request PUT \ --url https://api.pictory.ai/pictoryapis/v2/video/storyboard/YOUR_JOB_ID/elements \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '[ { "id": "backgroundElement_20260306230202544742ecd40c041449dac21bb737c799201", "elementType": "backgroundElement", "type": "video", "startTime": 0, "duration": 7.6, "url": "https://your-cdn.com/new-background.mp4", "visualUrl": "https://your-cdn.com/new-background.mp4", "visualType": "video" } ]' ``` ```javascript JavaScript theme={null} // Get the element ID from the preview response renderParams.elements const elementId = renderParams.elements.find( el => el.elementType === 'backgroundElement' && el.startTime === 0 ).id; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/video/storyboard/${jobId}/elements`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify([ { id: elementId, elementType: 'backgroundElement', type: 'video', startTime: 0, duration: 7.6, url: 'https://your-cdn.com/new-background.mp4', visualUrl: 'https://your-cdn.com/new-background.mp4', visualType: 'video' } ]) } ); if (response.status === 204) { console.log('Background visual updated successfully'); } ``` ```python Python theme={null} import requests # Get the element ID from the preview response renderParams.elements element_id = next( el['id'] for el in render_params['elements'] if el['elementType'] == 'backgroundElement' and el['startTime'] == 0 ) response = requests.put( f'https://api.pictory.ai/pictoryapis/v2/video/storyboard/{job_id}/elements', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=[ { 'id': element_id, 'elementType': 'backgroundElement', 'type': 'video', 'startTime': 0, 'duration': 7.6, 'url': 'https://your-cdn.com/new-background.mp4', 'visualUrl': 'https://your-cdn.com/new-background.mp4', 'visualType': 'video' } ] ) if response.status_code == 204: print('Background visual updated successfully') ``` ```php PHP theme={null} $elementId, 'elementType' => 'backgroundElement', 'type' => 'video', 'startTime' => 0, 'duration' => 7.6, 'url' => 'https://your-cdn.com/new-background.mp4', 'visualUrl' => 'https://your-cdn.com/new-background.mp4', 'visualType' => 'video' ] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 204) { echo "Background visual updated successfully\n"; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { jobID := "YOUR_JOB_ID" // Use the element ID from the preview response renderParams.elements elementID := "backgroundElement_20260306230202544742ecd40c041449dac21bb737c799201" url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v2/video/storyboard/%s/elements", jobID) payload := []map[string]interface{}{ { "id": elementID, "elementType": "backgroundElement", "type": "video", "startTime": 0, "duration": 7.6, "url": "https://your-cdn.com/new-background.mp4", "visualUrl": "https://your-cdn.com/new-background.mp4", "visualType": "video", }, } requestBody, _ := json.Marshal(payload) req, err := http.NewRequest("PUT", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode == 204 { fmt.Println("Background visual updated successfully") } } ``` ### Update Scene Text ```javascript JavaScript theme={null} // Find the scene text element to update const sceneTextElement = renderParams.elements.find( el => el.elementType === 'SceneText' && el.startTime === 7.6 ); const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/video/storyboard/${jobId}/elements`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify([ { id: sceneTextElement.id, elementType: 'SceneText', type: 'text', startTime: sceneTextElement.startTime, duration: sceneTextElement.duration, text: 'By automating repetitive tasks, AI frees up time for creative work and strategic thinking.', textLines: [] // Clear textLines so line breaks are recalculated } ]) } ); if (response.status === 204) { console.log('Scene text updated successfully'); } ``` ```python Python theme={null} import requests # Find the scene text element to update scene_text = next( el for el in render_params['elements'] if el['elementType'] == 'SceneText' and el['startTime'] == 7.6 ) response = requests.put( f'https://api.pictory.ai/pictoryapis/v2/video/storyboard/{job_id}/elements', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=[ { 'id': scene_text['id'], 'elementType': 'SceneText', 'type': 'text', 'startTime': scene_text['startTime'], 'duration': scene_text['duration'], 'text': 'By automating repetitive tasks, AI frees up time for creative work and strategic thinking.', 'textLines': [] # Clear textLines so line breaks are recalculated } ] ) if response.status_code == 204: print('Scene text updated successfully') ``` ### Update Background Music ```javascript JavaScript theme={null} const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/video/storyboard/${jobId}/elements`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify([ { id: 'bgMusic', elementType: 'audioElement', type: 'audio', startTime: 0, duration: 100000, url: 'https://your-cdn.com/new-background-music.mp3' } ]) } ); if (response.status === 204) { console.log('Background music updated successfully'); } ``` ```python Python theme={null} import requests response = requests.put( f'https://api.pictory.ai/pictoryapis/v2/video/storyboard/{job_id}/elements', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=[ { 'id': 'bgMusic', 'elementType': 'audioElement', 'type': 'audio', 'startTime': 0, 'duration': 100000, 'url': 'https://your-cdn.com/new-background-music.mp3' } ] ) if response.status_code == 204: print('Background music updated successfully') ``` ### Update Multiple Elements ```javascript JavaScript theme={null} // Update background visuals, scene text, and music in a single request const elements = renderParams.elements; // Find elements by type 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 fetch( `https://api.pictory.ai/pictoryapis/v2/video/storyboard/${jobId}/elements`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify([ // Replace scene 1 background with a custom image { id: bg1.id, elementType: 'backgroundElement', type: 'image', startTime: 0, duration: 7.6, url: 'https://your-cdn.com/intro-background.jpg', visualUrl: 'https://your-cdn.com/intro-background.jpg', visualType: 'image' }, // Replace scene 2 background with a custom video { id: bg2.id, elementType: 'backgroundElement', type: 'video', startTime: 7.6, duration: 11.1, url: 'https://your-cdn.com/demo-footage.mp4', visualUrl: 'https://your-cdn.com/demo-footage.mp4', visualType: 'video' }, // Update scene text — set textLines to null so line breaks are recalculated { id: sceneText.id, elementType: 'SceneText', type: 'text', startTime: 7.6, duration: 11.1, text: 'Updated scene text with highlighted keywords for emphasis.', textLines: [] }, // Replace background music { id: 'bgMusic', elementType: 'audioElement', type: 'audio', startTime: 0, duration: 100000, url: 'https://your-cdn.com/corporate-music.mp3' } ]) } ); if (response.status === 204) { console.log('All elements updated successfully'); } ``` ```python Python theme={null} import requests elements = render_params['elements'] # Find elements by type 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'https://api.pictory.ai/pictoryapis/v2/video/storyboard/{job_id}/elements', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=[ # Replace scene 1 background with a custom image { 'id': bg1['id'], 'elementType': 'backgroundElement', 'type': 'image', 'startTime': 0, 'duration': 7.6, 'url': 'https://your-cdn.com/intro-background.jpg', 'visualUrl': 'https://your-cdn.com/intro-background.jpg', 'visualType': 'image' }, # Replace scene 2 background with a custom video { 'id': bg2['id'], 'elementType': 'backgroundElement', 'type': 'video', 'startTime': 7.6, 'duration': 11.1, 'url': 'https://your-cdn.com/demo-footage.mp4', 'visualUrl': 'https://your-cdn.com/demo-footage.mp4', 'visualType': 'video' }, # Update scene text — set textLines to None so line breaks are recalculated { 'id': scene_text['id'], 'elementType': 'SceneText', 'type': 'text', 'startTime': 7.6, 'duration': 11.1, 'text': 'Updated scene text with highlighted keywords for emphasis.', 'textLines': None }, # Replace background music { 'id': 'bgMusic', 'elementType': 'audioElement', 'type': 'audio', 'startTime': 0, 'duration': 100000, 'url': 'https://your-cdn.com/corporate-music.mp3' } ] ) if response.status_code == 204: print('All elements updated successfully') ``` *** ## Important Notes You cannot add new elements to the storyboard. Only elements that already exist in the `renderParams.elements` array (returned from the storyboard preview job) can be updated. Elements are matched by their unique `id`. After creating a storyboard preview and polling the job to completion, the response includes `renderParams.elements` — an array of all elements with their `id` values. Element IDs are auto-generated unique strings. Always retrieve the actual IDs from the response rather than constructing them manually. When you update an element, the properties you provide are merged with the existing element properties. Properties you do not include in the update remain unchanged. For example, updating only the `url` of a background element will keep its existing `loop`, `mute`, `objectMode`, and other properties. When updating the `text` property of a `SceneText` or `layerItem` text element, you must set `textLines` to an empty array `[]`. The `textLines` array contains pre-calculated line break positions and character coordinates that are specific to the original text. If you change the text but keep the old `textLines`, the rendering engine will use stale layout data, causing incorrect line breaks and character positioning. Setting `textLines` to `[]` ensures the system recalculates the layout during preview and video rendering. Scene text (`SceneText`) elements support `` tags for keyword highlighting. For example: `"This is important text"` will render "important" with the element's `keywordColor`. Once elements are updated, the changes are saved to the storyboard job. When you subsequently render the video using [Render from Preview](/api-reference/videos/render-from-preview), the updated elements will be used in the final render. *** ## Related APIs Generate a storyboard preview with elements to update Render the final video from the updated storyboard Render video directly from storyboard input Check job status and retrieve render params # Create Storyboard Preview Source: https://docs.pictory.ai/api-reference/videos/create-storyboard-preview POST https://api.pictory.ai/pictoryapis/v2/video/storyboard Generate a video storyboard preview to review scenes, visuals, and settings before final rendering ## Overview The Storyboard Preview API generates a preview of your video project, allowing you to review the storyboard structure, scene breakdown, visual selections, and configuration before committing to the final render. This is an essential step in the video creation workflow that helps you validate your content and make adjustments without incurring full rendering costs. The storyboard preview creates a project with scene thumbnails and metadata. To generate the final rendered video, use the [Render from Preview API](/api-reference/videos/render-from-preview) after reviewing and approving the preview. **Recommended Workflow:** Create Preview → Review Scenes → Make Adjustments → Render Final Video *** ## Render Workflow Options | Workflow | API | When to Use | | ----------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- | | **Create Preview** | **This API** | Generate preview to review scenes before rendering | | **Render from Preview** | [Render from Preview](/api-reference/videos/render-from-preview) | Render preview as-is without modifications | | **Render with Modifications** | [Render Video](/api-reference/videos/render-video) | Modify preview elements before rendering | | **Render Saved Project** | [Render Project](/api-reference/videos/render-project) | Render existing project created in App or via API | | **Direct Render** | [Render Storyboard Video](/api-reference/videos/render-storyboard-video) | Skip preview, render directly from input | *** ## Preview vs. Final Render | Aspect | Storyboard Preview | Final Render | | ------------ | --------------------------------------------- | ------------------------ | | **Purpose** | Review and validate content | Produce final video file | | **Output** | Scene thumbnails, metadata, project structure | Full HD video file | | **Speed** | Fast | Slower | | **Cost** | Lower resource usage | Full rendering resources | | **Editable** | Yes, make changes before render | Video is final | | **Use Case** | Content approval, iteration | Final delivery | *** ## Use Cases Preview how your text will be split into scenes before rendering Review AI-selected visuals and backgrounds for each scene Identify scenes that need text or visual changes Share previews with stakeholders before final render Validate content before using rendering resources Quickly iterate on content and settings Test templates and brand settings before production Prepare multiple video projects for batch rendering *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v2/video/storyboard ``` *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body Parameters Name for your video project (alphanumeric, spaces, underscores, and hyphens only, max 150 characters) Width of the generated video in pixels. Must be provided together with `videoHeight`. Height of the generated video in pixels. Must be provided together with `videoWidth`. Video aspect ratio. Options: `1:1`, `16:9`, `9:16`, `4:5` Language of the text content. **Allowed values:** `zh`, `nl`, `en`, `fr`, `de`, `hi`, `it`, `ja`, `ko`, `mr`, `pt`, `ru`, `es`, `ta` `zh` Chinese, `nl` Dutch, `en` English, `fr` French, `de` German, `hi` Hindi, `it` Italian, `ja` Japanese, `ko` Korean, `mr` Marathi, `pt` Portuguese, `ru` Russian, `es` Spanish, `ta` Tamil Whether to save the project in your Pictory account for later editing. Default: `false` URL where the completed storyboard preview output will be sent via POST request when finished (max 500 characters) Custom data object that will be included in the webhook POST payload when the job completes. Use this to pass through any metadata (e.g., internal IDs, tracking info) that you need in your webhook handler. ```json theme={null} { "webhookInput": { "internalId": "abc-123", "source": "campaign-builder" } } ``` ID of a template to use. Get from [Get Templates](/api-reference/templates/get-templates) API. Key-value object for template variables. Only applicable when `templateId` is provided. ```json theme={null} { "variables": { "headline": "Welcome to Our Product", "subheadline": "Discover Amazing Features", "ctaText": "Learn More" } } ``` ID of the brand to apply. Get from [Get Video Brands](/api-reference/branding/get-video-brands) API. Cannot be used with `brandName`. Name of the brand to apply. Cannot be used with `brandId`. Name of the smart layout to apply. Get available layouts from [Get Smart Layouts](/api-reference/smartlayouts/get-smart-layouts) API. Cannot be used with `smartLayoutId`. ID of the smart layout to apply. Get from [Get Smart Layouts](/api-reference/smartlayouts/get-smart-layouts) API. Cannot be used with `smartLayoutName`. **Experimental:** This field is currently experimental. In the future, all storyboards will automatically use the latest version, and this field will be ignored. There is no need to remove it from your requests when that happens. Specifies which storyboard version to use. Set to `v3` to create a latest Pictory storyboard. Omit this field to use the classic storyboard. Options: * `v3` — Uses the latest Pictory storyboard * Omit or any other value — Uses the classic/legacy storyboard ID of a saved text style for subtitles. Get from [Get Text Styles](/api-reference/branding/get-text-styles) API. Cannot be used with `subtitleStyleName`. Name of a saved text style for subtitles. Cannot be used with `subtitleStyleId`. Inline subtitle style configuration. See [subtitleStyle Object](#subtitlestyle-object). AWS connection ID for accessing private S3 assets. Get from [Get AWS Connections](/api-reference/aws-integration/get-aws-connections) API. Vimeo connection ID for uploading videos directly to Vimeo. Get from [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) API. Array of destination configurations for uploading the generated video (max 5). See [destinations Array](#destinations-array). Voice-over configuration for AI narration. See [voiceOver Object](#voiceover-object). Background music configuration for the video. See [backgroundMusic Object](#backgroundmusic-object). Logo overlay configuration. See [logo Object](#logo-object). Array of scene objects containing the content to convert into video. Required unless using `templateId`. See [scenes Array](#scenes-array). AI avatar configuration for presenter-style videos. See [avatar Object](#avatar-object). *** ## voiceOver Object Voice-over configuration for AI narration. ```json theme={null} { "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "Brian", "speed": 100, "amplificationLevel": 0 } ] } } ``` The following fields are nested inside the top-level `voiceOver` object. For example, `voiceOver.enabled` means `enabled` is a property of the `voiceOver` object. Enable or disable voice-over. Array of AI voice configurations (max 10). See [aiVoices Array](#aivoices-array). External voice file configuration. See [externalVoice Object](#externalvoice-object). Cannot be used with `aiVoices`. ### aiVoices Array The following fields are nested inside each element of the `voiceOver.aiVoices` array. For example, `voiceOver.aiVoices[].speaker` means `speaker` is a property of each item in the `aiVoices` array, which itself is nested inside the `voiceOver` object. The `[]` notation indicates an array element. Voice speaker name (e.g., "Brian", "Emma"). Get available voices from [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) API. Speech speed from 50 to 200. Default: `100` Volume level from -1 to 1. Default: `0` ElevenLabs premium voice settings. See [premiumVoiceSettings Object](#premiumvoicesettings-object). ### premiumVoiceSettings Object ```json theme={null} { "premiumVoiceSettings": { "modelId": "eleven_multilingual_v2", "stability": "50%", "similarityBoost": "75%", "style": "50%", "useSpeakerBoost": true } } ``` The following fields are nested inside `premiumVoiceSettings`, which is a property of each `voiceOver.aiVoices[]` element. For example, `voiceOver.aiVoices[].premiumVoiceSettings.modelId` means `modelId` is inside `premiumVoiceSettings`, inside each voice in the `aiVoices` array, inside the `voiceOver` object. ElevenLabs model. Options: `eleven_v3`, `eleven_multilingual_v2`, `eleven_flash_v2_5`, `eleven_turbo_v2_5`, `eleven_turbo_v2`, `eleven_flash_v2`, `eleven_multilingual_v1`, `eleven_monolingual_v1` Voice stability as percentage (e.g., "50%") Enable speaker boost enhancement. Similarity boost as percentage (e.g., "75%") Voice style as percentage (e.g., "50%") ### externalVoice Object ```json theme={null} { "externalVoice": { "voiceUrl": "https://example.com/voiceover.mp3", "syncVoice": true, "amplificationLevel": 0 } } ``` The following fields are nested inside the `voiceOver.externalVoice` object. For example, `voiceOver.externalVoice.voiceUrl` means `voiceUrl` is a property of `externalVoice`, which itself is nested inside the `voiceOver` object. URL to external voice audio file. Auto-synchronize voice with video subtitles. Audio amplification level from -1 to 1. Default: `0` *** ## avatar Object AI avatar configuration for creating presenter-style videos with lifelike avatars that lip-sync to AI narration. Avatars require voice-over to be enabled. Use the [Get Avatars](/api-reference/avatars/get-avatars) API to retrieve available `avatarId` values. Unique identifier for the avatar look (e.g., "Annie\_expressive12\_public"). Get from [Get Avatars](/api-reference/avatars/get-avatars) API. Preset position for the avatar. Options: `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Default: `bottom-right` Avatar width as percentage (e.g., "20%", "25%", "30%"). Must be an integer percentage from 0% to 100%. Default: `"20%"` Corner rounding. Options: `0` (square), `50` (rounded), `100` (circular) Border color in RGBA format (e.g., "rgba(255,255,255,1)", "rgba(0,0,0,1)", "rgba(132,44,254,1)") Border width in pixels. Minimum: 0 Background color in RGBA format (e.g., "rgba(255,255,255,1)", "rgba(0,0,0,0.5)") Hide the avatar globally across all scenes. When `true`, the entire avatar pipeline is skipped. Can be overridden per scene. Default: `false` Avatar settings can be overridden at the scene level. See [scenes\[\].avatar Object](#scenesavatar-object) for scene-specific customization. *** ## backgroundMusic Object Background music configuration for the video. ```json theme={null} { "backgroundMusic": { "enabled": true, "autoMusic": true, "volume": 0.5 } } ``` The following fields are nested inside the top-level `backgroundMusic` object. For example, `backgroundMusic.enabled` means `enabled` is a property of the `backgroundMusic` object. Enable or disable background music. Auto-select background music. Cannot be used with `musicUrl`. URL to custom background music file. Cannot be used with `autoMusic`. Volume level from 0 to 1. Default: `0.5` Time clips to use from the music (max 10). Each clip has `start` and `end` numbers in seconds. ```json theme={null} { "clips": [ { "start": 0, "end": 30 }, { "start": 60, "end": 90 } ] } ``` *** ## logo Object Logo overlay configuration. ```json theme={null} { "logo": { "url": "https://example.com/logo.png", "position": "top-right", "width": "15%" } } ``` The following fields are nested inside the top-level `logo` object. For example, `logo.url` means `url` is a property of the `logo` object. URL to logo image file. Logo position. Options: `top-left`, `top-right`, `top-center`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Logo width as percentage (e.g., "15%") *** ## subtitleStyle Object Inline subtitle style configuration. Can be used at video level or scene level. ```json theme={null} { "subtitleStyle": { "fontFamily": "Montserrat", "fontSize": 48, "color": "rgba(255,255,255,1)", "position": "bottom-center", "alignment": "center" } } ``` The following fields are nested inside the `subtitleStyle` object. For example, `subtitleStyle.fontFamily` means `fontFamily` is a property of the `subtitleStyle` object. This object can be used at the top level or inside `scenes[]` as `scenes[].subtitleStyle`. URL to custom font file. When provided, `fontFamily` is required. Font family name. Required when `fontUrl` is provided. See [Supported Font Families](#supported-font-families). Font size in pixels. Minimum: 1 Text color in RGBA format (e.g., "rgba(255,255,255,1)") Background color in RGBA format (e.g., "rgba(0,0,0,0.5)"). Shadow color. Shadow width as percentage (e.g., "10%") Color for highlighted keywords in RGBA format (e.g., "rgba(255,255,0,1)"). Text position. Options: `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Text alignment. Options: `left`, `center`, `right` Text decorations. Options: `bold`, `underline`, `italics`, `linethrough` ```json theme={null} { "decorations": ["bold", "italics"] } ``` Text case. Options: `uppercase`, `lowercase`, `capitalize`, `smallcapitalize` Paragraph width as percentage (e.g., "80%") Text animations (max 2: one entry, one exit). See [animations Array](#animations-array). ```json theme={null} { "animations": [ { "name": "fade", "type": "entry", "speed": "fast", "futureWords": "hidden" }, { "name": "fade", "type": "exit", "speed": "fast" } ] } ``` See [Text Animations Guide](/guides/captions/text-animations) for all animation types and examples. ### animations Array The following fields are nested inside each element of the `subtitleStyle.animations` array. For example, `subtitleStyle.animations[].name` means `name` is a property of each item in the `animations` array, which itself is nested inside the `subtitleStyle` object. The `[]` notation indicates an array element. Animation name. Options: `none`, `fade`, `drift`, `wipe`, `text reveal`, `elastic`, `typewriter`, `blur`, `bulletin` Animation type. Options: `entry`, `exit` Animation speed. Options: `slow`, `medium`, `fast`, `custom` Custom speed value (min 0.5). Required when speed is `custom`. Animation direction. Options: `up`, `down`, `left`, `right` Writing style for text animations. Options: `character`, `word`, `line`, `paragraph` Controls how upcoming words appear on screen. Only available with `fade` and `blur` entry animations. When AI voiceover is enabled, words sync with the spoken audio. Options: * `"hidden"` — Upcoming words are invisible, words appear only when spoken * `"subtle"` — Upcoming words are faintly visible, current word is highlighted * `"prominent"` — All text is clearly visible, spoken word is highlighted brighter See [Text Animations Guide](/guides/captions/text-animations) for examples. ### Supported Font Families `Anton`, `Archivo Narrow`, `Arial`, `Averia Libre`, `Barlow`, `Barlow Black`, `Bebas Neue`, `Calibri`, `Caprasimo`, `Capriola`, `Carter One`, `Caveat`, `Chakra Petch`, `Chewy`, `Comfortaa`, `Courier Prime`, `Dancing Script`, `Dangrek`, `Delius Unicase`, `DM Sans`, `Grandstander`, `Gruppo`, `Heartbeat`, `Helvetica`, `Helvetica Neue Medium`, `JM Modern`, `Josefin Sans`, `Julius Sans One`, `Laisha`, `Lato`, `Lato Extrabold`, `Lexend`, `LT Wave`, `Manrope`, `Merriweather`, `Montserrat`, `Moon dance`, `Mustard`, `Notosans`, `Opensans`, `Optik`, `Party Confetti`, `Patua One`, `Playfair Display`, `Plus Jakarta Sans`, `Poppins`, `Poppins Extrabold`, `Proxima Nova`, `Quicksand`, `Raleway`, `Raleway Black`, `Raleway Thin`, `Roboto`, `Rokkitt`, `Rowdies`, `Russo One`, `Satisfy`, `Sora`, `Source Sans Pro`, `Space Grotesk`, `Space mono`, `Special Elite`, `Strive`, `Titillium Web`, `Titillium Web Black`, `Ubuntu`, `Unbounded`, `Work Sans`, `Noto Sans JP`, `Noto Serif JP`, `Noto Sans Tamil`, `Noto Sans Devanagari`, `Noto Serif Devanagari` *** ## destinations Array Array of destination configurations for uploading the generated video (max 5). ### Vimeo Destination ```json theme={null} { "destinations": [ { "type": "vimeo", "folder_uri": "/users/12345/projects/67890", "content_rating": ["safe"], "privacy": { "view": "unlisted", "embed": "public", "download": false } } ] } ``` The following fields are nested inside each element of the top-level `destinations` array. For example, `destinations[].type` means `type` is a property of each item in the `destinations` array. The `[]` notation indicates an array element. Must be `vimeo` Vimeo folder URI. Content rating. Options: `violence`, `drugs`, `language`, `nudity`, `advertisement`, `safe`, `unrated` ```json theme={null} { "content_rating": ["safe"] } ``` Privacy settings. See [Vimeo Privacy Object](#vimeo-privacy-object). ### Vimeo Privacy Object The following fields are nested inside the `privacy` object of each `destinations[]` element. For example, `destinations[].privacy.view` means `view` is a property of `privacy`, which itself is nested inside each item of the `destinations` array. View access. Options: `anybody`, `contacts`, `disable`, `nobody`, `password`, `unlisted`, `users` Embed access. Options: `private`, `public`, `whitelist` Comment access. Options: `anybody`, `contacts`, `nobody` Allow adding to albums/channels. Allow downloads. *** ## scenes Array Each scene in the `scenes` array can contain various content sources and settings. ```json theme={null} { "scenes": [ { "story": "Welcome to our product showcase. Discover amazing features.", "createSceneOnEndOfSentence": true, "highlightKeywords": true, "sceneTransition": "fade", "background": { "visualUrl": "https://example.com/background.mp4", "type": "video", "settings": { "mute": true, "loop": true } } } ] } ``` Each scene can only have ONE content source. Choose one of: `story`, `storyCoPilot`, `blogUrl`, `pptUrl`, `audioUrl`, or `videoUrl`. The following fields are nested inside each element of the top-level `scenes` array. For example, `scenes[].story` means `story` is a property of each item in the `scenes` array. The `[]` notation indicates an array element. Text content for creating video from text (max 15000 characters). AI story generation configuration. See [storyCoPilot Object](#storycopilot-object). URL to blog/article content. URL to PowerPoint presentation file. URL to audio file for audio-to-video conversion. Requires `audioLanguage`. URL to video file for video repurposing. Requires `audioLanguage`. Create new scene on each new line. Default: `false` Create new scene at end of each sentence. Default: `false` Maximum number of caption lines displayed simultaneously. Integer from `1` to `4`. When omitted, all caption lines for the scene are displayed at once. * `1` — Single line (ideal for TikTok/Reels) * `2` — Two lines (standard for most content) * `3` — Three lines (educational/detailed content) * `4` — Four lines (information-heavy content) Cannot be used with smart layouts (`smartLayoutId` or `smartLayoutName`). When AI voiceover is enabled, caption lines sync with the spoken audio. See [Dynamic Captions Guide](/guides/captions/dynamic-captions). Highlight keywords in subtitles. Hide subtitles for this scene. Minimum duration for the scene in seconds. Pause duration at the end of the scene in seconds. Transition effect. Options: `none`, `wipeup`, `wipedown`, `wipeleft`, `wiperight`, `smoothleft`, `smoothright`, `radial`, `circlecrop`, `hblur`, `fade` Language of audio content. Required for `audioUrl` and `videoUrl`. **Allowed values:** `en-US`, `en-AU`, `en-GB`, `en-IN`, `en-IE`, `en-AB`, `en-WL`, `fr-CA`, `fr-FR`, `de-CH`, `de-DE`, `it-IT`, `es-ES`, `es-US`, `nl-NL`, `pt-BR`, `ja-JP`, `ko-KR`, `ru-RU`, `hi-IN`, `ta-IN`, `mr-IN` `en-US` English (US), `en-AU` English (Australia), `en-GB` English (UK), `en-IN` English (India), `en-IE` English (Ireland), `en-AB` English (Arabic accent), `en-WL` English (Wales), `fr-CA` French (Canada), `fr-FR` French (France), `de-CH` German (Switzerland), `de-DE` German (Germany), `it-IT` Italian (Italy), `es-ES` Spanish (Spain), `es-US` Spanish (US), `nl-NL` Dutch (Netherlands), `pt-BR` Portuguese (Brazil), `ja-JP` Japanese (Japan), `ko-KR` Korean (Korea), `ru-RU` Russian (Russia), `hi-IN` Hindi (India), `ta-IN` Tamil (India), `mr-IN` Marathi (India) Use speaker notes from PowerPoint. Only valid with `pptUrl`. Enable AI-generated slide animations for PowerPoint presentations. When `true`, the AI analyzes each slide and generates motion graphics animations (zoom, pan, fade effects) that bring static slides to life. Only valid with `pptUrl`. Whether story text contains SSML markup. Only valid with `story`. Default: `false` Separate caption text for the scene (max 3000 characters). Requires `story`. Language of caption text. Only valid with `caption`. **Allowed values:** `zh`, `nl`, `en`, `fr`, `de`, `hi`, `it`, `ja`, `ko`, `mr`, `pt`, `ru`, `es`, `ta` `zh` Chinese, `nl` Dutch, `en` English, `fr` French, `de` German, `hi` Hindi, `it` Italian, `ja` Japanese, `ko` Korean, `mr` Marathi, `pt` Portuguese, `ru` Russian, `es` Spanish, `ta` Tamil ID of a saved text style for this scene's subtitles. Cannot be used with `subtitleStyleName`. Name of a saved text style for this scene's subtitles. Cannot be used with `subtitleStyleId`. Inline subtitle style for this scene. Overrides video-level subtitle style. See [subtitleStyle Object](#subtitlestyle-object). Voice-over configuration specific to this scene. Overrides video-level settings. See [scenes\[\].voiceOver Object](#scenesvoiceover-object). Background music configuration specific to this scene. See [scenes\[\].backgroundMusic Object](#scenesbackgroundmusic-object). Avatar customization specific to this scene. Overrides global avatar settings. See [scenes\[\].avatar Object](#scenesavatar-object). Background media configuration for this scene. See [background Object](#background-object). Array of B-roll background configurations. See [backgroundBrolls Array](#backgroundbrolls-array). Array of background images to use as a corpus (max 100). See [backgroundCorpus Array](#backgroundcorpus-array). Transcription of input video or audio. Required when repurposing `audioUrl` or `videoUrl`. See [transcript Array](#transcript-array). Settings for repurposing media content. Only valid with `audioUrl` or `videoUrl`. See [mediaRepurposeSettings Object](#mediarepurposesettings-object). Configuration for manipulating template scenes. Only available when `templateId` is provided. See [templateOverride Object](#templateoverride-object). *** ## storyCoPilot Object AI story generation configuration for creating video scripts automatically. ```json theme={null} { "storyCoPilot": { "prompt": "How AI is transforming video creation", "videoType": "Explainer", "duration": 60, "platform": "YouTube", "tone": "informative" } } ``` The following fields are nested inside the `storyCoPilot` object, which is a property of each `scenes[]` element. For example, `scenes[].storyCoPilot.prompt` means `prompt` is a property of `storyCoPilot`, which itself is nested inside each item of the `scenes` array. Topic or description for AI to generate content (1-5000 characters). Type of video to generate. Options: `Explainer`, `Marketing`, `Internal Communication`, `Tutorial`, `Product`. Default: `Explainer` Target duration in seconds (1-600). Target platform. Options: `YouTube`, `TikTok`, `Instagram`, `Facebook`, `LinkedIn`, `Twitter` Tone of the generated content. Options: `professional`, `casual`, `friendly`, `informative`, `persuasive`, `exciting`, `educational`, `humorous`, `serious`, `conversational` *** ## scenes\[].voiceOver Object Voice-over configuration specific to a scene. Overrides video-level voice-over settings. ```json theme={null} { "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Emma", "speed": 100 }] } } ``` The following fields are nested inside the `voiceOver` object at the scene level. For example, `scenes[].voiceOver.enabled` means `enabled` is a property of `voiceOver`, which itself is nested inside each item of the `scenes` array. This is separate from the top-level `voiceOver` object. Enable or disable voice-over for this scene. Array of AI voice configurations (max 10). Same structure as [aiVoices Array](#aivoices-array). External voice configuration with `voiceUrl`, `clips` array, and `amplificationLevel`. *** ## scenes\[].backgroundMusic Object Background music configuration specific to a scene. ```json theme={null} { "backgroundMusic": { "enabled": true } } ``` The following fields are nested inside the `backgroundMusic` object at the scene level. For example, `scenes[].backgroundMusic.enabled` means `enabled` is a property of `backgroundMusic`, which itself is nested inside each item of the `scenes` array. This is separate from the top-level `backgroundMusic` object. Enable or disable background music for this scene. *** ## scenes\[].avatar Object Scene-level avatar customization. Overrides global [avatar Object](#avatar-object) settings for this specific scene. Scene-level settings override global avatar configuration. Any property not specified at the scene level inherits from the global avatar settings. ```json theme={null} { "avatar": { "position": "top-left", "width": "25%", "hide": false } } ``` Preset position for the avatar in this scene. Options: `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Distance from top edge as percentage (e.g., "10%", "25%", "50%"). Integer percentages only (0%-100%). Cannot be used with `position`. Distance from left edge as percentage (e.g., "5%", "50%", "80%"). Integer percentages only (0%-100%). Cannot be used with `position`. Avatar width as percentage for this scene (e.g., "20%", "25%", "30%"). Must be an integer percentage from 0% to 100%. Corner rounding for this scene. Options: `0` (square), `50` (rounded), `100` (circular) Border color in RGBA format for this scene (e.g., "rgba(255,255,255,1)", "rgba(0,0,0,1)", "rgba(132,44,254,1)") Border width in pixels for this scene. Minimum: 0 Background color in RGBA format for this scene (e.g., "rgba(255,255,255,1)", "rgba(0,0,0,0.5)") Hide avatar for this specific scene (`true` or `false`) **Important:** You cannot change `avatarId` at the scene level. The same avatar must be used throughout the video. You can only override styling and positioning properties. *** ## background Object Background media configuration for a scene. ```json theme={null} { "background": { "visualUrl": "https://example.com/video.mp4", "type": "video", "settings": { "mute": true, "loop": true } } } ``` Background can only have ONE of: `visualUrl`, `color`, or `aiVisual` - not multiple. The following fields are nested inside the `background` object, which is a property of each `scenes[]` element. For example, `scenes[].background.visualUrl` means `visualUrl` is a property of `background`, which itself is nested inside each item of the `scenes` array. URL to background video or image. Cannot be used with `color` or `aiVisual`. Solid background color in RGBA format (e.g., "rgba(0,0,0,1)"). Cannot be used with `visualUrl` or `aiVisual`. AI-generated visual configuration. Cannot be used with `visualUrl` or `color`. See [aiVisual Object](#aivisual-object). Media type. Options: `video`, `image`. **Required** when `aiVisual` is provided — determines whether an AI image or AI video clip is generated. Search filter for auto-selecting visuals. See [searchFilter Object](#searchfilter-object). Time clips to use from background video. Only valid when `type` is `video` and `visualUrl` is provided. Each clip has `start` and `end` numbers in seconds. ```json theme={null} { "clips": [ { "start": 0, "end": 10 }, { "start": 20, "end": 35 } ] } ``` Background playback settings. See [background.settings Object](#backgroundsettings-object). ### aiVisual Object AI-generated visual configuration. Use with `background.type` set to `"image"` for AI-generated images or `"video"` for AI-generated video clips. The following fields are nested inside the `aiVisual` object, which is a property of `scenes[].background`. For example, `scenes[].background.aiVisual.prompt` means `prompt` is a property of `aiVisual`, which itself is nested inside `background`, inside each item of the `scenes` array. ```json theme={null} { "background": { "type": "image", "aiVisual": { "prompt": "A serene mountain landscape at sunset", "model": "seedream3.0", "mediaStyle": "photorealistic" } } } ``` ```json theme={null} { "background": { "type": "video", "aiVisual": { "prompt": "Aerial drone shot slowly flying over a tropical coastline", "model": "pixverse5.5", "videoDuration": "8s" } } } ``` ```json theme={null} { "background": { "type": "video", "aiVisual": { "prompt": "A car driving through a desert highway at golden hour", "model": "pixverse5.5", "videoDuration": "8s", "firstFrameImageUrl": "https://example.com/car-on-highway.jpg" } } } ``` ```json theme={null} { "background": { "type": "image", "aiVisual": { "prompt": "A modern office workspace with natural lighting", "model": "seedream3.0", "mediaStyle": "photorealistic", "referenceImageUrl": "https://example.com/office-reference.jpg" } } } ``` ```json theme={null} { "scenes": [ { "story": "AI is poised to significantly impact educators and course creators on social media. By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency. This allows creators to focus on higher-level strategies and ensures a cohesive brand presence.", "createSceneOnNewLine": true, "createSceneOnEndOfSentence": true "background": { "type": "video", "aiVisual": { "model": "pixverse5.5", "visualContinuity": true } } } ] } ``` Text prompt describing the visual to generate (max 500 characters). If omitted, the system auto-generates a prompt from the scene's story text. When a story is split into multiple scenes (using `createSceneOnEndOfSentence` or `createSceneOnNewLine`), this prompt acts as a **creative direction** for the entire video rather than a prompt for a specific scene. The system uses it to guide the auto-generated prompts for each individual scene, ensuring a consistent visual tone. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. For example: `"Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field"`. AI model to use. The valid options depend on `background.type`: **Image models** (when `type` is `"image"`): `flux-schnell`, `seedream3.0`, `nanobanana`, `nanobanana-pro` **Video models** (when `type` is `"video"`): `pixverse5.5`, `veo3.1_fast`, `veo3.1` Visual style for generated images. Only applicable when `type` is `"image"`. Options: `photorealistic`, `artistic`, `cartoon`, `minimalist`, `vintage`, `futuristic` Duration of the generated video clip. Only applicable when `type` is `"video"`. Valid values depend on the model: * `pixverse5.5`: `"5s"`, `"8s"`, `"10s"` (default: `"5s"`) * `veo3.1_fast`: `"4s"`, `"6s"`, `"8s"` (default: `"4s"`) * `veo3.1`: `"4s"`, `"6s"`, `"8s"` (default: `"4s"`) If omitted or invalid, the model's shortest duration is used. When set to `true`, enables visual continuity between consecutive AI-generated scenes. The system automatically uses the output of the previous scene as a reference for the next scene, creating smooth visual transitions. For video scenes, the last frame of the previous video is used. For image scenes, the generated image of the previous scene is used. This field works with both `"video"` and `"image"` types. Visual continuity applies in two scenarios: * **Within the same story:** Consecutive scenes that were split from the same original story use the previous scene's output as a reference for the next scene. * **Across consecutive stories:** Continuity is also maintained between the last scene of one story and the first scene of the next story, enabling seamless transitions across story boundaries. In both cases, `visualContinuity` must be set to `true` for the system to use the previous scene's output as a reference. If `visualContinuity` is not set or is `false`, the following behavior applies for consecutive scenes from the same story: * **Image scenes:** All user-provided reference images (such as `referenceImageUrl`) are cleared, and visuals are generated independently. * **Video scenes:** Only `firstFrameImageUrl` is cleared if it was provided. However, `referenceImageUrls` are preserved and still used for generation. This field has no effect on the first scene in a sequence. When continuity is active and the previous scene's output is unavailable, the system falls back to generating the visual without a reference image. URL of an image to use as the first frame of the generated video clip. The AI model generates a video that begins from this image and transitions into the content described in the prompt. Only applicable when `type` is `"video"`. Cannot be used together with `referenceImageUrls`. URL of a reference image to guide the style and composition of the generated image. Only applicable when `type` is `"image"`. For video generation, use `firstFrameImageUrl` or `referenceImageUrls` instead. An array of 1–2 reference image URLs to guide the style and composition of the generated video clip. Only applicable when `type` is `"video"`. Cannot be used together with `firstFrameImageUrl`. For image generation, use `referenceImageUrl` instead. When using `referenceImageUrls` (without `firstFrameImageUrl`) with the `veo3.1` or `veo3.1_fast` models, the video duration is automatically set to `"8s"`. #### Reference Image Fields — Quick Reference | Field | Applicable Type | Description | | -------------------- | --------------- | ------------------------------------------------------- | | `firstFrameImageUrl` | `"video"` only | Image used as the starting frame of the generated video | | `referenceImageUrl` | `"image"` only | Single reference image for AI image generation | | `referenceImageUrls` | `"video"` only | 1–2 reference images for AI video generation | **Mutual exclusivity rules:** * `firstFrameImageUrl` and `referenceImageUrls` cannot be provided together. Use one or the other. * `referenceImageUrl` is only for image generation. For video generation, use `firstFrameImageUrl` or `referenceImageUrls`. * `firstFrameImageUrl` and `referenceImageUrls` are only for video generation. For image generation, use `referenceImageUrl`. #### Image Models — AI Credit Cost | Model | AI Credits (per image) | Best For | | ---------------- | ---------------------- | ----------------------------- | | `flux-schnell` | 0.6 | Reliable for basic layouts | | `seedream3.0` | 2 | Reliable for text and numbers | | `nanobanana` | 4 | Excels at details | | `nanobanana-pro` | 14 | Superior cinematic quality | #### Video Models — AI Credit Cost Credits are charged **per second** of video generated. | Model | AI Credits (per second) | Best For | | ------------- | ----------------------- | --------------------------- | | `pixverse5.5` | 1.6 | Reliable for basic motion | | `veo3.1_fast` | 10 | Efficient cinematic quality | | `veo3.1` | 20 | Superior cinematic quality | For detailed guides on configuring AI-generated visuals, see: * [AI-Generated Scene Background Images](/guides/ai-generated-visuals/background-images) * [AI-Generated Scene Background Video Clips](/guides/ai-generated-visuals/background-videos) ### searchFilter Object ```json theme={null} { "searchFilter": { "category": "Nature/Landscapes", "query": "mountain sunset", "keywords": ["nature", "mountains", "sunset"], "libraries": ["story_blocks", "getty"] } } ``` The following fields are nested inside the `searchFilter` object, which is a property of `scenes[].background`. For example, `scenes[].background.searchFilter.category` means `category` is a property of `searchFilter`, which itself is nested inside `background`, inside each item of the `scenes` array. Category filter for media search. See [Visual Filter Categories](#visual-filter-categories). Custom search query to find relevant visuals for your scene (max 3000 characters). Use descriptive terms to get more accurate background media results (e.g., "business team collaborating in modern office", "aerial view of city skyline at sunset"). Keywords for search (max 10, each 2-100 characters). ```json theme={null} { "keywords": ["business", "technology", "innovation"] } ``` Media libraries to search. Options: `story_blocks`, `getty` ```json theme={null} { "libraries": ["story_blocks", "getty"] } ``` ### background.settings Object ```json theme={null} { "settings": { "mute": true, "loop": true, "zoomAndPan": false } } ``` The following fields are nested inside the `settings` object, which is a property of `scenes[].background`. For example, `scenes[].background.settings.mute` means `mute` is a property of `settings`, which itself is nested inside `background`, inside each item of the `scenes` array. Mute audio from background media. Loop the background media. Apply zoom and pan effects. Cannot be used with `kenBurnsEffect`. Apply Ken Burns effect (images only). Cannot be used with `zoomAndPan`. ### Visual Filter Categories **Aerial:** `Aerial`, `Aerial/Coastal_and_Marine`, `Aerial/Infrastructure`, `Aerial/Natural_Landscapes`, `Aerial/Urban_Landscapes` **Animals:** `Animals`, `Animals/Farm_Animals`, `Animals/Marine_Life`, `Animals/Pets`, `Animals/Wildlife` **Business:** `Business_and_Professions`, `Business_and_Professions/Business_Concepts`, `Business_and_Professions/Office_Work`, `Business_and_Professions/Professions` **Effects:** `Effects`, `Effects/Chemical_Reactions`, `Effects/Explosions`, `Effects/Fire_and_Smoke`, `Effects/Glitches`, `Effects/Lighting_Effects`, `Effects/Particles` **Food:** `Food_and_Beverage`, `Food_and_Beverage/Beverages`, `Food_and_Beverage/Food_Preparation`, `Food_and_Beverage/Meals` **Graphics:** `Graphics`, `Graphics/Backgrounds`, `Graphics/Effects`, `Graphics/Patterns` **Historical:** `Historical`, `Historical/Eras`, `Historical/Events`, `Historical/Figures` **Holidays:** `Holidays_and_Celebrations`, `Holidays_and_Celebrations/Cultural_Celebrations`, `Holidays_and_Celebrations/Festivals`, `Holidays_and_Celebrations/Life_Events` **Lifestyle:** `Lifestyle`, `Lifestyle/Health_and_Fitness`, `Lifestyle/Hobbies`, `Lifestyle/Home_and_Family` **Medical:** `Medical` **Nature:** `Nature`, `Nature/Landscapes`, `Nature/Plants_and_Trees`, `Nature/Sunrises_and_Sunsets`, `Nature/Waterfalls`, `Nature/Weather` **People:** `People`, `People/Activities`, `People/Groups`, `People/Portraits` **Places:** `Places_and_Landmarks`, `Places_and_Landmarks/Rural_Areas`, `Places_and_Landmarks/Tourist_Attractions`, `Places_and_Landmarks/Urban_Areas` **Sports:** `Sports_and_Recreation`, `Sports_and_Recreation/Individual_Sports`, `Sports_and_Recreation/Outdoor_Activities`, `Sports_and_Recreation/Team_Sports` **Technology:** `Technology`, `Technology/Devices`, `Technology/Innovation` *** ## backgroundBrolls Array Array of B-roll background configurations for creating dynamic scene transitions. ```json theme={null} { "backgroundBrolls": [ { "visualUrl": "https://example.com/broll.mp4", "type": "video", "brollClip": { "start": 0, "end": 5 }, "settings": { "mute": true } } ] } ``` Each B-roll item supports all [background Object](#background-object) properties plus: The following fields are nested inside each element of the `scenes[].backgroundBrolls` array. For example, `scenes[].backgroundBrolls[].brollClip` means `brollClip` is a property of each item in the `backgroundBrolls` array, which itself is nested inside each item of the `scenes` array. Time clip configuration for B-roll. Contains `start` (number, required) and `end` (number, required) in seconds. *** ## backgroundCorpus Array Array of background images to use as a corpus for the scene (max 100 items). ```json theme={null} { "backgroundCorpus": [ { "visualUrl": "https://example.com/image1.jpg", "type": "image", "prefer": true }, { "visualUrl": "https://example.com/image2.jpg", "type": "image" } ] } ``` The following fields are nested inside each element of the `scenes[].backgroundCorpus` array. For example, `scenes[].backgroundCorpus[].visualUrl` means `visualUrl` is a property of each item in the `backgroundCorpus` array, which itself is nested inside each item of the `scenes` array. URL to background image. Must be `image`. Mark as preferred image for selection. *** ## transcript Array Transcription of input video or audio content. ```json theme={null} { "transcript": [ { "speakerId": 1, "words": [ { "word": "Hello", "start_time": 0.0, "end_time": 0.5, "is_pause": false, "is_filler": false } ] } ] } ``` The following fields are nested inside the `scenes[].transcript` array. For example, `scenes[].transcript[].speakerId` means `speakerId` is a property of each item in the `transcript` array, which itself is nested inside each item of the `scenes` array. Fields like `scenes[].transcript[].words[].word` go one level deeper — `word` is inside each item of the `words` array, which is inside each `transcript` element. Speaker identifier. Array of word objects. The word text. Start time in seconds (min 0). End time in seconds (must be >= start\_time). Whether this is a pause. Whether this is a filler word. Speaker identifier for this word. *** ## mediaRepurposeSettings Object Settings for repurposing audio or video content. ```json theme={null} { "mediaRepurposeSettings": { "highlightLength": 60, "removeFillerWords": true, "removeSilences": true, "silenceThresholdSeconds": 2 } } ``` The following fields are nested inside the `mediaRepurposeSettings` object, which is a property of each `scenes[]` element. For example, `scenes[].mediaRepurposeSettings.highlightLength` means `highlightLength` is a property of `mediaRepurposeSettings`, which itself is nested inside each item of the `scenes` array. Length of highlight in seconds (5-180). Remove filler words from the content. Remove silences from the content. Silence threshold in seconds (0-10). *** ## templateOverride Object Configuration for manipulating template scenes. Only available when `templateId` is provided. ```json theme={null} { "templateOverride": { "sceneId": "scene-123", "subtitles": [ { "text": "Updated subtitle text", "minimumDuration": 3 } ], "layers": [ [ { "layerId": "text-layer-1", "type": "text", "text": "Updated text content" } ] ] } } ``` The following fields are nested inside the `templateOverride` object, which is a property of each `scenes[]` element. For example, `scenes[].templateOverride.sceneId` means `sceneId` is a property of `templateOverride`, which itself is nested inside each item of the `scenes` array. ID of the existing scene to modify. Cannot be used with `scenePosition`. Position of the existing scene to modify (1-based). Cannot be used with `sceneId`. New position for a newly inserted scene (1-based). Only for new scenes. Insert new scene after this scene ID. Only for new scenes. Cannot be used with `newScenePosition`. Insert new scene before this scene ID. Only for new scenes. Cannot be used with `newScenePosition` or `insertAfterSceneId`. Replace the scene with this ID. Only for new scenes. Cannot be used with `newScenePosition`. Replace the scene at this position (1-based). Only for new scenes. Cannot be used with `newScenePosition` or `replaceSceneId`. Base scene ID to copy settings from. Only for new scenes. Base scene position to copy settings from (1-based). Only for new scenes. Cannot be used with `baseSceneId`. Delete this scene. Only for existing scenes. Copy this scene. Only for existing scenes. Array of subtitle configurations (max 500). Only for existing scenes. See [templateOverride.subtitles Array](#templateoverridesubtitles-array). Array of layer configurations (max 500). Only for existing scenes. See [templateOverride.layers Array](#templateoverridelayers-array). **Template Override Rules:** * `templateOverride` is only available when `templateId` is provided * Use `sceneId` OR `scenePosition` to identify existing scenes, not both * Properties like `newScenePosition`, `insertAfterSceneId`, `insertBeforeSceneId`, `replaceSceneId`, `baseSceneId` are only for new scenes * Properties like `deleteScene`, `copyScene`, `subtitles`, `layers` are only for existing scenes ### templateOverride.subtitles Array ```json theme={null} { "subtitles": [ { "text": "Your subtitle text here", "minimumDuration": 3, "styleId": "style-123" } ] } ``` The following fields are nested inside each element of the `scenes[].templateOverride.subtitles` array. For example, `scenes[].templateOverride.subtitles[].text` means `text` is a property of each item in the `subtitles` array, which itself is nested inside `templateOverride`, inside each item of the `scenes` array. Subtitle text (1-40000 characters). Minimum duration in seconds. ID of saved text style. Name of saved text style. Inline style configuration. See [subtitleStyle Object](#subtitlestyle-object). ### templateOverride.layers Array Layers allow you to customize text, image, and video elements within template scenes. ```json theme={null} { "layers": [ [ { "layerId": "headline", "type": "text", "text": "Updated headline" }, { "layerId": "logo", "type": "image", "url": "https://example.com/logo.png" } ] ] } ``` The following fields are nested inside the `scenes[].templateOverride.layers` structure, which is an array of arrays. The `[][]` notation indicates that `layers` is an array of arrays — the outer `[]` represents each subtitle group, and the inner `[]` represents each layer element within that group. For example, `scenes[].templateOverride.layers[][].layerId` means `layerId` is a property of each layer element inside the nested array structure. ID of the layer to modify (max 100 characters). Layer type. Options: `text`, `image`, `video` Text content (max 40000 characters). For `text` type only. Media URL. For `image` or `video` type only. Text style ID. For `text` type only. Text style name. For `text` type only. Inline text style. See [subtitleStyle Object](#subtitlestyle-object). For `text` type only. Delete this layer. *** ## Response When the storyboard request is successfully submitted, a job is created and a job ID is returned. Use this job ID to poll the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) endpoint for the completed storyboard preview. Indicates whether the request was successful Contains the storyboard job information Unique identifier for the storyboard preview job. Use this to track the job status and retrieve results via the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "success": true, "data": { "jobId": "a1d36612-326d-4b81-aece-411f8aed4c70" } } ``` ```json 400 - Invalid Request Body theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "scenes", "errors": "scenes is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ## Next Steps Once you have the `jobId`, poll the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) endpoint to check the job status and retrieve the completed storyboard preview. Use a polling interval of 10–30 seconds. When the job completes, you will receive: * **`renderParams`** — The rendering data for the video. To customize the video, update this data and use the [Render Video](/api-reference/videos/render-video) API to render with your modifications. To update the preview with changes, save the updated data using the [Update Storyboard Elements](/api-reference/video-storyboard/update-storyboard-elements) API. * **`storyboard`** — The processed input storyboard. If you want to change anything in the original storyboard request, use this processed response to make a new request with updates — it will process faster since the storyboard is already broken down into scenes, voices, and other components. * **`previewUrl`** — The preview URL for the video storyboard. Use this to view the preview in a browser or embed it in an iframe for app integrations. See the [Embed Preview Player](/integrations/storyboard-video-preview/embed-preview-player) guide for details. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key ### Basic Text to Video Preview ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/video/storyboard \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "videoName": "demo_text_to_video", "voiceOver": { "enabled": true, "aiVoices": [{"speaker": "Brian", "speed": 100}] }, "scenes": [ { "story": "AI is transforming how we create content. It automates tasks and saves time.", "createSceneOnEndOfSentence": true } ] }' | python -m json.tool ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'demo_text_to_video', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'AI is transforming how we create content. It automates tasks and saves time.', createSceneOnEndOfSentence: true }] }) }); const result = await response.json(); console.log('Job ID:', result.data.jobId); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'demo_text_to_video', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'AI is transforming how we create content. It automates tasks and saves time.', 'createSceneOnEndOfSentence': True }] } ) result = response.json() print(f"Job ID: {result['data']['jobId']}") ``` ```php PHP theme={null} 'demo_text_to_video', 'voiceOver' => [ 'enabled' => true, 'aiVoices' => [['speaker' => 'Brian', 'speed' => 100]] ], 'scenes' => [[ 'story' => 'AI is transforming how we create content. It automates tasks and saves time.', 'createSceneOnEndOfSentence' => true ]] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); curl_close($ch); $result = json_decode($response, true); echo "Job ID: " . $result['data']['jobId'] . PHP_EOL; ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v2/video/storyboard" payload := map[string]interface{}{ "videoName": "demo_text_to_video", "voiceOver": map[string]interface{}{ "enabled": true, "aiVoices": []map[string]interface{}{ {"speaker": "Brian", "speed": 100}, }, }, "scenes": []map[string]interface{}{ { "story": "AI is transforming how we create content. It automates tasks and saves time.", "createSceneOnEndOfSentence": true, }, }, } requestBody, _ := json.Marshal(payload) req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Job ID:", data["jobId"]) } ``` ### Preview with Smart Layout ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'smart_layout_video', smartLayoutName: 'modern minimalist', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Emma', speed: 100 }] }, scenes: [{ story: 'Professional video creation has never been easier. Let AI handle the visuals.', createSceneOnEndOfSentence: true }] }) }); ``` ```python Python theme={null} response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'smart_layout_video', 'smartLayoutName': 'modern minimalist', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Emma', 'speed': 100}] }, 'scenes': [{ 'story': 'Professional video creation has never been easier. Let AI handle the visuals.', 'createSceneOnEndOfSentence': True }] } ) ``` ### Preview with AI Story Generation ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'ai_generated_script', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Matthew', speed: 100 }] }, scenes: [{ storyCoPilot: { prompt: 'Benefits of remote work for modern businesses', videoType: 'Explainer', duration: 90, platform: 'LinkedIn', tone: 'professional' }, createSceneOnEndOfSentence: true }] }) }); ``` ```python Python theme={null} response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'ai_generated_script', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Matthew', 'speed': 100}] }, 'scenes': [{ 'storyCoPilot': { 'prompt': 'Benefits of remote work for modern businesses', 'videoType': 'Explainer', 'duration': 90, 'platform': 'LinkedIn', 'tone': 'professional' }, 'createSceneOnEndOfSentence': True }] } ) ``` ### Preview with AI-Generated Background Image ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'ai_background_image_video', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'Experience the beauty of nature through AI-generated imagery.', createSceneOnEndOfSentence: true, background: { type: 'image', aiVisual: { model: 'seedream3.0', mediaStyle: 'photorealistic' }, settings: { zoomAndPan: true } } }] }) }); ``` ```python Python theme={null} response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'ai_background_image_video', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'Experience the beauty of nature through AI-generated imagery.', 'createSceneOnEndOfSentence': True, 'background': { 'type': 'image', 'aiVisual': { 'model': 'seedream3.0', 'mediaStyle': 'photorealistic' }, 'settings': { 'zoomAndPan': True } } }] } ) ``` ### Preview with AI-Generated Background Video Clip ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'ai_background_video_clip', aspectRatio: '16:9', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'The future of technology is unfolding before our eyes.', createSceneOnEndOfSentence: true, background: { type: 'video', aiVisual: { prompt: 'Futuristic cityscape at night with flying vehicles and neon signs', model: 'pixverse5.5', videoDuration: '8s' } } }] }) }); ``` ```python Python theme={null} response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'ai_background_video_clip', 'aspectRatio': '16:9', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'The future of technology is unfolding before our eyes.', 'createSceneOnEndOfSentence': True, 'background': { 'type': 'video', 'aiVisual': { 'prompt': 'Futuristic cityscape at night with flying vehicles and neon signs', 'model': 'pixverse5.5', 'videoDuration': '8s' } } }] } ) ``` ### Full Featured Preview Example ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'full_featured_video', aspectRatio: '16:9', language: 'en', saveProject: true, brandName: 'My Brand', // Background music backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, // AI Voice-over voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100, amplificationLevel: 0 }] }, // Logo logo: { url: 'https://example.com/logo.png', position: 'top-right', width: '12%' }, // Subtitle styling subtitleStyle: { fontFamily: 'Montserrat', fontSize: 48, color: 'rgba(255,255,255,1)', position: 'bottom-center', alignment: 'center' }, // Scenes scenes: [{ story: 'Welcome to our company. We create innovative solutions. Join us on this journey.', createSceneOnEndOfSentence: true, highlightKeywords: true, sceneTransition: 'fade' }] }) }); ``` ```python Python theme={null} response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'full_featured_video', 'aspectRatio': '16:9', 'language': 'en', 'saveProject': True, 'brandName': 'My Brand', # Background music 'backgroundMusic': { 'enabled': True, 'autoMusic': True, 'volume': 0.3 }, # AI Voice-over 'voiceOver': { 'enabled': True, 'aiVoices': [{ 'speaker': 'Brian', 'speed': 100, 'amplificationLevel': 0 }] }, # Logo 'logo': { 'url': 'https://example.com/logo.png', 'position': 'top-right', 'width': '12%' }, # Subtitle styling 'subtitleStyle': { 'fontFamily': 'Montserrat', 'fontSize': 48, 'color': 'rgba(255,255,255,1)', 'position': 'bottom-center', 'alignment': 'center' }, # Scenes 'scenes': [{ 'story': 'Welcome to our company. We create innovative solutions. Join us on this journey.', 'createSceneOnEndOfSentence': True, 'highlightKeywords': True, 'sceneTransition': 'fade' }] } ) ``` *** ## Error Handling ```json theme={null} { "success": false, "error": { "code": "INVALID_REQUEST", "message": "videoName is required" } } ``` **Solution:** Ensure `videoName` and `scenes` are provided. ```json theme={null} { "success": false, "error": { "message": "Scene must have exactly one of: story, storyCoPilot, blogUrl, pptUrl, audioUrl, or videoUrl" } } ``` **Solution:** Each scene can only have one content source. ```json theme={null} { "success": false, "error": { "message": "maxSubtitleLines is not allowed when smartLayoutId or smartLayoutName is provided" } } ``` **Solution:** Remove `maxSubtitleLines` when using smart layouts. ```json theme={null} { "message": "Unauthorized" } ``` **Solution:** Check your API key is valid and correctly formatted in the Authorization header. **Solution:** Implement exponential backoff and space out requests. *** ## Related Guides Generate AI images as scene backgrounds Generate AI video clips as scene backgrounds Add professional AI narration Apply professional visual layouts Generate scripts with AI Add music to your videos Apply consistent branding Customize caption formatting *** ## Next Steps After creating your storyboard preview: Monitor preview generation progress and retrieve results Convert approved preview into final video file Modify scenes or settings before rendering Retrieve full project information ### Supporting APIs List available AI voices for your preview View saved subtitle styles List available brands Find stock visuals for your scenes # Render Video from Preview Source: https://docs.pictory.ai/api-reference/videos/render-from-preview PUT https://api.pictory.ai/pictoryapis/v2/video/render/{storyboardjobid} Render final video from an existing storyboard preview job output ## Overview The Render from Preview API generates the final video file from an existing storyboard preview. This endpoint is part of the recommended two-step workflow: first create a preview to review and validate content, then render the final video once approved. This endpoint requires a completed storyboard preview job ID. First use the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview) to generate a preview, then use this endpoint to render the final video. **Recommended Workflow:** Create Preview → Review Scenes → Make Adjustments (optional) → Render Final Video *** ## Render Workflow Options | Workflow | API | When to Use | | ----------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | | **Create Preview** | [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) | Generate preview to review scenes before rendering | | **Render from Preview** | **This API** | Render preview as-is without modifications | | **Render with Modifications** | [Render Video](/api-reference/videos/render-video) | Modify preview elements before rendering | | **Render Saved Project** | [Render Project](/api-reference/videos/render-project) | Render existing project created in App or via API | | **Direct Render** | [Render Storyboard Video](/api-reference/videos/render-storyboard-video) | Skip preview, render directly from input | ### Benefits of Two-Step Workflow Review scenes, visuals, and timing before committing to render Only render videos that have been reviewed and approved Make adjustments to the preview before final render Ensure content meets requirements before production *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v2/video/render/{storyboardjobid} ``` *** ## Request Parameters ### Path Parameters The job ID returned from a completed [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) request. This ID identifies the storyboard configuration to render. ### Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body Parameters The request body allows you to override or add settings from the original storyboard preview. All parameters are optional - if not provided, the original preview settings are used. URL where the completed video output will be sent via POST request when finished (max 500 characters). Overrides the webhook from the preview. *** ## Response When the render request is successfully submitted, a job is created and a job ID is returned. Indicates whether the request was successful Unique identifier for the render job. Use this to track the job status and retrieve results via the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "success": true, "data": { "jobId": "265a7c1a-4985-4058-9208-68114f131a2b" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ## Next Steps Once you have the `jobId`, poll the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint to check the render status and retrieve the output URLs when complete. Use a polling interval of 10–30 seconds. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key and `STORYBOARD_JOB_ID` with the job ID from your preview request. ### Basic Render from Preview ```bash cURL theme={null} curl --request PUT \ --url https://api.pictory.ai/pictoryapis/v2/video/render/74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9 \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{}' | python -m json.tool ``` ```javascript JavaScript theme={null} const storyboardJobId = '74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/video/render/${storyboardJobId}`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({}) } ); const data = await response.json(); console.log('Render Job ID:', data.data.jobId); ``` ```python Python theme={null} import requests storyboard_job_id = '74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9' response = requests.put( f'https://api.pictory.ai/pictoryapis/v2/video/render/{storyboard_job_id}', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={} ) data = response.json() print('Render Job ID:', data['data']['jobId']) ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { storyboardJobId := "74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9" url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v2/video/render/%s", storyboardJobId) requestBody, _ := json.Marshal(map[string]interface{}{}) req, err := http.NewRequest("PUT", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Render Job ID:", data["jobId"]) } ``` ### Render with Webhook Notification ```javascript JavaScript theme={null} const storyboardJobId = '74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/video/render/${storyboardJobId}`, { method: 'PUT', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ webhook: 'https://your-server.com/video-complete' }) } ); const data = await response.json(); console.log('Render Job ID:', data.data.jobId); ``` ```python Python theme={null} import requests storyboard_job_id = '74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9' response = requests.put( f'https://api.pictory.ai/pictoryapis/v2/video/render/{storyboard_job_id}', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'webhook': 'https://your-server.com/video-complete' } ) data = response.json() print('Render Job ID:', data['data']['jobId']) ``` ```php PHP theme={null} 'https://your-server.com/video-complete' ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode === 200) { $data = json_decode($response, true); echo 'Render Job ID: ' . $data['data']['jobId'] . PHP_EOL; } else { echo 'Request failed with status ' . $httpCode . PHP_EOL; } ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { storyboardJobId := "74c0cc68-e1ed-42f3-a527-8bfc0b46bdb9" url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v2/video/render/%s", storyboardJobId) payload := map[string]interface{}{ "webhook": "https://your-server.com/video-complete", } requestBody, _ := json.Marshal(payload) req, err := http.NewRequest("PUT", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Render Job ID:", data["jobId"]) } ``` ### Complete Two-Step Workflow ```javascript JavaScript theme={null} const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.pictory.ai/pictoryapis'; // Step 1: Create storyboard preview async function createPreview() { const response = await fetch(`${API_BASE}/v2/video/storyboard`, { method: 'POST', headers: { 'Authorization': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_video', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'Welcome to our product demo. We will show you amazing features.', createSceneOnEndOfSentence: true }] }) }); const data = await response.json(); return data.data.jobId; } // Step 2: Wait for preview to complete async function waitForJob(jobId) { while (true) { const response = await fetch(`${API_BASE}/v1/jobs/${jobId}`, { headers: { 'Authorization': API_KEY } }); const data = await response.json(); if (data.data.status === 'completed') { console.log('Preview ready!'); return data.data; } if (data.data.status === 'failed') { throw new Error('Job failed'); } await new Promise(resolve => setTimeout(resolve, 5000)); } } // Step 3: Render final video from preview async function renderFromPreview(storyboardJobId) { const response = await fetch(`${API_BASE}/v2/video/render/${storyboardJobId}`, { method: 'PUT', headers: { 'Authorization': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ webhook: 'https://your-server.com/video-complete' }) }); const data = await response.json(); return data.data.jobId; } // Execute workflow async function main() { // Create preview const previewJobId = await createPreview(); console.log('Preview Job ID:', previewJobId); // Wait for preview await waitForJob(previewJobId); // Render final video const renderJobId = await renderFromPreview(previewJobId); console.log('Render Job ID:', renderJobId); // Wait for render const result = await waitForJob(renderJobId); console.log('Video URL:', result.videoURL); } main(); ``` ```python Python theme={null} import requests import time API_KEY = 'YOUR_API_KEY' API_BASE = 'https://api.pictory.ai/pictoryapis' def create_preview(): """Step 1: Create storyboard preview""" response = requests.post( f'{API_BASE}/v2/video/storyboard', headers={ 'Authorization': API_KEY, 'Content-Type': 'application/json' }, json={ 'videoName': 'my_video', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'Welcome to our product demo. We will show you amazing features.', 'createSceneOnEndOfSentence': True }] } ) data = response.json() return data['data']['jobId'] def wait_for_job(job_id): """Step 2: Wait for job to complete""" while True: response = requests.get( f'{API_BASE}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) data = response.json() status = data['data']['status'] if status == 'completed': print('Job completed!') return data['data'] if status == 'failed': raise Exception('Job failed') time.sleep(5) def render_from_preview(storyboard_job_id): """Step 3: Render final video from preview""" response = requests.put( f'{API_BASE}/v2/video/render/{storyboard_job_id}', headers={ 'Authorization': API_KEY, 'Content-Type': 'application/json' }, json={ 'webhook': 'https://your-server.com/video-complete' } ) data = response.json() return data['data']['jobId'] def main(): # Create preview preview_job_id = create_preview() print(f'Preview Job ID: {preview_job_id}') # Wait for preview wait_for_job(preview_job_id) # Render final video render_job_id = render_from_preview(preview_job_id) print(f'Render Job ID: {render_job_id}') # Wait for render result = wait_for_job(render_job_id) print(f'Video URL: {result["videoURL"]}') if __name__ == '__main__': main() ``` *** ## Error Handling ```json theme={null} { "success": false, "error": { "code": "INVALID_REQUEST", "message": "Invalid storyboard job ID format" } } ``` **Solution:** Ensure you are using the correct job ID format (UUID) from a storyboard preview request. ```json theme={null} { "success": false, "error": { "code": "NOT_FOUND", "message": "Storyboard job not found" } } ``` **Solution:** Verify the storyboard job ID exists and belongs to your account. The preview job must be completed before rendering. ```json theme={null} { "success": false, "error": { "message": "Storyboard preview is not yet completed" } } ``` **Solution:** Wait for the storyboard preview job to complete before calling this endpoint. Use the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) API to check status. ```json theme={null} { "message": "Unauthorized" } ``` **Solution:** Check your API key is valid and correctly formatted in the Authorization header. *** ## Related APIs Generate preview before rendering (Step 1) Direct render without preview step Monitor rendering progress List saved video projects # Render Project Source: https://docs.pictory.ai/api-reference/videos/render-project POST https://api.pictory.ai/pictoryapis/v2/projects/{projectid}/render Initiate the rendering process for a project to generate the final video output based on the current project configuration and content. ## Overview Initiate the rendering process for an existing Pictory project to generate the final video output. This endpoint creates a render job that processes your project's scenes, audio, subtitles, and visual effects into a complete video file. The rendering happens asynchronously, and you can track progress using the job ID returned in the response. This endpoint renders existing Pictory projects. Projects can be created via the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview) with `saveProject: true`, or directly in the [Pictory App](https://app.pictory.ai). **Automation Workflow:** Create or edit projects in the Pictory App, then use this API to trigger rendering programmatically as part of your automation pipeline. *** ## Render Workflow Options | Workflow | API | When to Use | | ----------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | | **Create Preview** | [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) | Generate preview to review scenes before rendering | | **Render from Preview** | [Render from Preview](/api-reference/videos/render-from-preview) | Render preview as-is without modifications | | **Render with Modifications** | [Render Video](/api-reference/videos/render-video) | Modify preview elements before rendering | | **Render Saved Project** | **This API** | Render existing project created in App or via API | | **Direct Render** | [Render Storyboard Video](/api-reference/videos/render-storyboard-video) | Skip preview, render directly from input | ### When to Use This Endpoint Render projects created and edited in the Pictory web application Render projects saved via Storyboard API with saveProject: true Integrate project rendering into automated workflows Re-render projects after manual edits in the App *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v2/projects/{projectid}/render ``` *** ## Request Parameters ### Path Parameters The unique identifier of the project to render. Get from [Get Projects](/api-reference/projects/get-projects) API. ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response When the render request is successfully submitted, a job is created and a job ID is returned. Indicates whether the request was successful Unique identifier for the render job. Use this to track the job status and retrieve results via the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "success": true, "data": { "jobId": "265a7c1a-4985-4058-9208-68114f131a2b" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "success": false, "message": "Project not found" } ``` ```json 409 - Conflict theme={null} { "success": false, "message": "Project is already being rendered" } ``` ## Next Steps Once you have the `jobId`, poll the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint to check the render status and retrieve the output URLs when complete. Use a polling interval of 10–30 seconds. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID/render' \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests project_id = "20251222191648030d7df02f5b4054d4ca8831f1369459e25" url = f"https://api.pictory.ai/pictoryapis/v2/projects/{project_id}/render" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.post(url, headers=headers) result = response.json() if result.get('success'): job_id = result['data']['jobId'] print(f"Render job started: {job_id}") else: print(f"Failed to start render: {result.get('message')}") ``` ```javascript JavaScript theme={null} const projectId = '20251222191648030d7df02f5b4054d4ca8831f1369459e25'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}/render`, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const result = await response.json(); if (result.success) { const jobId = result.data.jobId; console.log(`Render job started: ${jobId}`); } else { console.log(`Failed to start render: ${result.message}`); } ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" ) type RenderResponse struct { Success bool `json:"success"` Data struct { JobID string `json:"jobId"` } `json:"data"` Message string `json:"message"` } func main() { projectID := "20251222191648030d7df02f5b4054d4ca8831f1369459e25" url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v2/projects/%s/render", projectID) req, err := http.NewRequest("POST", url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result RenderResponse json.Unmarshal(body, &result) if result.Success { fmt.Printf("Render job started: %s\n", result.Data.JobID) } else { fmt.Printf("Failed to start render: %s\n", result.Message) } } ``` *** ## Usage Notes **Asynchronous Processing**: Rendering happens asynchronously. The API returns immediately with a job ID. Use the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) API to poll and track rendering progress. **Rendering Time**: Rendering time varies based on project complexity, video length, number of scenes, and applied effects. Simple projects may render in minutes, while complex projects can take longer. **Job Status Polling**: Poll the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) API periodically (recommended: every 10–30 seconds) to determine when the video is ready. The completed job response includes the `videoURL`. *** ## Rendering Behavior ### What Happens During Rendering 1. **Job Creation**: A render job is created and queued in the rendering system 2. **Processing**: The system processes all project elements: * Compiles all scene visuals (images/videos) * Generates and synchronizes voice-over audio * Applies text overlays and subtitle formatting * Processes transitions and effects * Combines all elements into the final video 3. **Encoding**: The final video is encoded in the specified format and quality 4. **Completion**: The job status changes to complete, and video URLs become available *** ## Code Examples: Poll for Completion ```javascript JavaScript theme={null} async function waitForVideo(jobId, apiKey) { const maxAttempts = 60; const pollInterval = 10000; // 10 seconds for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`, { headers: { 'Authorization': apiKey } } ); const data = await response.json(); const status = data.data.status; console.log(`Attempt ${attempt + 1}: Status = ${status}`); if (status === 'completed') { console.log('Video URL:', data.data.videoURL); return data.data; } if (status === 'failed') { throw new Error('Video rendering failed: ' + JSON.stringify(data)); } await new Promise(resolve => setTimeout(resolve, pollInterval)); } throw new Error('Timeout waiting for video to render'); } // Usage const projectId = 'your-project-id'; const response = await fetch( `https://api.pictory.ai/pictoryapis/v2/projects/${projectId}/render`, { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY' } } ); const result = await response.json(); const videoResult = await waitForVideo(result.data.jobId, 'YOUR_API_KEY'); ``` ```python Python theme={null} import requests import time def wait_for_video(job_id, api_key, max_attempts=60, poll_interval=10): for attempt in range(max_attempts): response = requests.get( f'https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}', headers={'Authorization': api_key} ) data = response.json() status = data['data']['status'] print(f'Attempt {attempt + 1}: Status = {status}') if status == 'completed': print('Video URL:', data['data']['videoURL']) return data['data'] if status == 'failed': raise Exception(f'Video rendering failed: {data}') time.sleep(poll_interval) raise Exception('Timeout waiting for video to render') # Usage project_id = 'your-project-id' render_response = requests.post( f'https://api.pictory.ai/pictoryapis/v2/projects/{project_id}/render', headers={'Authorization': 'YOUR_API_KEY'} ) result = render_response.json() video_result = wait_for_video(result['data']['jobId'], 'YOUR_API_KEY') ``` *** ## Best Practices ### Polling Strategy 1. **Start Interval**: Begin with 10-second intervals for the first minute 2. **Increase Gradually**: Increase to 15-30 seconds for longer renders 3. **Maximum Attempts**: Set a reasonable timeout (e.g., 20-30 minutes) 4. **Exponential Backoff**: Use increasing intervals to reduce API calls ### Error Handling * **Retry Logic**: Implement automatic retry for transient failures * **Timeout Handling**: Set appropriate timeouts based on project complexity * **Status Validation**: Always check the response status before proceeding * **Fallback Strategy**: Have a plan for handling permanent failures *** ## Related APIs Monitor rendering progress List projects to get project IDs Get project details and video URL Create new video projects via API # Render Storyboard Video Source: https://docs.pictory.ai/api-reference/videos/render-storyboard-video POST https://api.pictory.ai/pictoryapis/v2/video/storyboard/render Render a final video from a storyboard with all scenes, visuals, voice-over, and effects ## Overview The Render Storyboard Video API generates the final rendered video file from your storyboard configuration. This endpoint processes all scenes, applies voice-over narration, background music, visual effects, and produces a complete video ready for distribution. This endpoint renders the final video directly. For a preview-first workflow, use the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview) to review scenes before rendering. **Direct Render:** Use this endpoint when you are confident in your content and want to skip the preview step, or when re-rendering a previously approved storyboard. *** ## Render Workflow Options | Workflow | API | When to Use | | ----------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | | **Create Preview** | [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) | Generate preview to review scenes before rendering | | **Render from Preview** | [Render from Preview](/api-reference/videos/render-from-preview) | Render preview as-is without modifications | | **Render with Modifications** | [Render Video](/api-reference/videos/render-video) | Modify preview elements before rendering | | **Render Saved Project** | [Render Project](/api-reference/videos/render-project) | Render existing project created in App or via API | | **Direct Render** | **This API** | Skip preview, render directly from input | *** ## Preview vs. Final Render | Aspect | Storyboard Preview | Final Render | | ------------ | --------------------------------------------- | ------------------------ | | **Purpose** | Review and validate content | Produce final video file | | **Output** | Scene thumbnails, metadata, project structure | Full HD video file | | **Speed** | Fast | Slower | | **Cost** | Lower resource usage | Full rendering resources | | **Editable** | Yes, make changes before render | Video is final | | **Use Case** | Content approval, iteration | Final delivery | *** ## Use Cases Generate final videos directly from text, articles, or presentations Render multiple videos programmatically at scale Integrate into automated content creation workflows Re-render videos after making adjustments to projects Generate videos from templates with variable content Create videos for different platforms with aspect ratios *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v2/video/storyboard/render ``` *** ## Request Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body Parameters The request body accepts the same parameters as the [Create Storyboard Preview API](/api-reference/videos/create-storyboard-preview#request-body-parameters). All parameters for video configuration, scenes, voice-over, background music, branding, and destinations are supported. Name for your video project (alphanumeric, spaces, underscores, and hyphens only, max 150 characters) Width of the generated video in pixels. Must be provided together with `videoHeight`. Height of the generated video in pixels. Must be provided together with `videoWidth`. Video aspect ratio. Options: `1:1`, `16:9`, `9:16`, `4:5` Language of the text content. **Allowed values:** `zh`, `nl`, `en`, `fr`, `de`, `hi`, `it`, `ja`, `ko`, `mr`, `pt`, `ru`, `es`, `ta` `zh` Chinese, `nl` Dutch, `en` English, `fr` French, `de` German, `hi` Hindi, `it` Italian, `ja` Japanese, `ko` Korean, `mr` Marathi, `pt` Portuguese, `ru` Russian, `es` Spanish, `ta` Tamil Whether to save the project in your Pictory account for later editing. Default: `false` URL where the completed video output will be sent via POST request when finished (max 500 characters) Custom data object that will be included in the webhook POST payload when the job completes. Use this to pass through any metadata (e.g., internal IDs, tracking info) that you need in your webhook handler. ```json theme={null} { "webhookInput": { "internalId": "abc-123", "source": "campaign-builder" } } ``` ID of a template to use. Get from [Get Templates](/api-reference/templates/get-templates) API. Key-value object for template variables. Only applicable when `templateId` is provided. ```json theme={null} { "variables": { "headline": "Welcome to Our Product", "subheadline": "Discover Amazing Features", "ctaText": "Learn More" } } ``` ID of the brand to apply. Get from [Get Video Brands](/api-reference/branding/get-video-brands) API. Cannot be used with `brandName`. Name of the brand to apply. Cannot be used with `brandId`. Name of the smart layout to apply. Get available layouts from [Get Smart Layouts](/api-reference/smartlayouts/get-smart-layouts) API. Cannot be used with `smartLayoutId`. ID of the smart layout to apply. Get from [Get Smart Layouts](/api-reference/smartlayouts/get-smart-layouts) API. Cannot be used with `smartLayoutName`. **Experimental:** This field is currently experimental. In the future, all storyboards will automatically use the latest version, and this field will be ignored. There is no need to remove it from your requests when that happens. Specifies which storyboard version to use. Set to `v3` to create a latest Pictory storyboard. Omit this field to use the classic storyboard. Options: * `v3` — Uses the latest Pictory storyboard * Omit or any other value — Uses the classic/legacy storyboard ID of a saved text style for subtitles. Get from [Get Text Styles](/api-reference/branding/get-text-styles) API. Cannot be used with `subtitleStyleName`. Name of a saved text style for subtitles. Cannot be used with `subtitleStyleId`. Inline subtitle style configuration. See [Create Storyboard Preview - subtitleStyle Object](/api-reference/videos/create-storyboard-preview#subtitlestyle-object). ```json theme={null} { "subtitleStyle": { "fontFamily": "Montserrat", "fontSize": 48, "color": "rgba(255,255,255,1)", "backgroundColor": "rgba(0,0,0,0.5)", "position": "bottom-center", "alignment": "center" } } ``` AWS connection ID for accessing private S3 assets. Get from [Get AWS Connections](/api-reference/aws-integration/get-aws-connections) API. Vimeo connection ID for uploading videos directly to Vimeo. Get from [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) API. Array of destination configurations for uploading the generated video (max 5). See [Create Storyboard Preview - destinations Array](/api-reference/videos/create-storyboard-preview#destinations-array). ```json theme={null} { "destinations": [ { "type": "vimeo", "folder_uri": "/users/12345/projects/67890", "content_rating": ["safe"], "privacy": { "view": "unlisted", "embed": "public", "download": false } } ] } ``` Voice-over configuration for AI narration. See [Create Storyboard Preview - voiceOver Object](/api-reference/videos/create-storyboard-preview#voiceover-object). ```json theme={null} { "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "Brian", "speed": 100, "amplificationLevel": 0 } ] } } ``` Background music configuration for the video. See [Create Storyboard Preview - backgroundMusic Object](/api-reference/videos/create-storyboard-preview#backgroundmusic-object). ```json theme={null} { "backgroundMusic": { "enabled": true, "autoMusic": true, "volume": 0.5 } } ``` Logo overlay configuration. See [Create Storyboard Preview - logo Object](/api-reference/videos/create-storyboard-preview#logo-object). ```json theme={null} { "logo": { "url": "https://example.com/logo.png", "position": "top-right", "width": "15%" } } ``` Array of scene objects containing the content to convert into video. Required unless using `templateId`. See [Create Storyboard Preview - scenes Array](/api-reference/videos/create-storyboard-preview#scenes-array). ```json theme={null} { "scenes": [ { "story": "Welcome to our product showcase. Discover amazing features.", "createSceneOnEndOfSentence": true, "highlightKeywords": true, "sceneTransition": "fade", "background": { "visualUrl": "https://example.com/background.mp4", "type": "video", "settings": { "mute": true, "loop": true } } } ] } ``` *** ## Response When the render request is successfully submitted, a job is created and a job ID is returned. Indicates whether the request was successful Unique identifier for the render job. Use this to track the job status and retrieve results via the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "success": true, "data": { "jobId": "265a7c1a-4985-4058-9208-68114f131a2b" } } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "scenes", "errors": "scenes is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ## Next Steps Once you have the `jobId`, poll the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint to check the render status and retrieve the output URLs when complete. Use a polling interval of 10–30 seconds. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key ### Basic Text to Video Render ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/video/storyboard/render \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "videoName": "my_rendered_video", "aspectRatio": "16:9", "voiceOver": { "enabled": true, "aiVoices": [{"speaker": "Brian", "speed": 100}] }, "scenes": [ { "story": "Welcome to our company. We create innovative solutions for modern businesses.", "createSceneOnEndOfSentence": true } ] }' | python -m json.tool ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_rendered_video', aspectRatio: '16:9', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'Welcome to our company. We create innovative solutions for modern businesses.', createSceneOnEndOfSentence: true }] }) }); const data = await response.json(); console.log('Job ID:', data.data.jobId); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'my_rendered_video', 'aspectRatio': '16:9', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'Welcome to our company. We create innovative solutions for modern businesses.', 'createSceneOnEndOfSentence': True }] } ) data = response.json() print('Job ID:', data['data']['jobId']) ``` ```php PHP theme={null} 'my_rendered_video', 'aspectRatio' => '16:9', 'voiceOver' => [ 'enabled' => true, 'aiVoices' => [['speaker' => 'Brian', 'speed' => 100]] ], 'scenes' => [[ 'story' => 'Welcome to our company. We create innovative solutions for modern businesses.', 'createSceneOnEndOfSentence' => true ]] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); echo 'Job ID: ' . $data['data']['jobId'] . PHP_EOL; ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v2/video/storyboard/render" payload := map[string]interface{}{ "videoName": "my_rendered_video", "aspectRatio": "16:9", "voiceOver": map[string]interface{}{ "enabled": true, "aiVoices": []map[string]interface{}{ {"speaker": "Brian", "speed": 100}, }, }, "scenes": []map[string]interface{}{ { "story": "Welcome to our company. We create innovative solutions for modern businesses.", "createSceneOnEndOfSentence": true, }, }, } requestBody, _ := json.Marshal(payload) req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Job ID:", data["jobId"]) } ``` ### Render with Full Configuration ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'complete_video_render', aspectRatio: '16:9', saveProject: true, webhook: 'https://your-server.com/webhook', // Branding brandName: 'My Brand', // Voice-over voiceOver: { enabled: true, aiVoices: [{ speaker: 'Emma', speed: 100, amplificationLevel: 0 }] }, // Background music backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, // Logo logo: { url: 'https://example.com/logo.png', position: 'top-right', width: '15%' }, // Subtitle styling subtitleStyle: { fontFamily: 'Montserrat', fontSize: 48, color: 'rgba(255,255,255,1)', position: 'bottom-center' }, // Scenes scenes: [{ story: 'Welcome to our company. We create innovative solutions. Join us on this journey.', createSceneOnEndOfSentence: true, highlightKeywords: true, sceneTransition: 'fade' }] }) }); const data = await response.json(); console.log('Job ID:', data.data.jobId); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard/render', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'complete_video_render', 'aspectRatio': '16:9', 'saveProject': True, 'webhook': 'https://your-server.com/webhook', # Branding 'brandName': 'My Brand', # Voice-over 'voiceOver': { 'enabled': True, 'aiVoices': [{ 'speaker': 'Emma', 'speed': 100, 'amplificationLevel': 0 }] }, # Background music 'backgroundMusic': { 'enabled': True, 'autoMusic': True, 'volume': 0.3 }, # Logo 'logo': { 'url': 'https://example.com/logo.png', 'position': 'top-right', 'width': '15%' }, # Subtitle styling 'subtitleStyle': { 'fontFamily': 'Montserrat', 'fontSize': 48, 'color': 'rgba(255,255,255,1)', 'position': 'bottom-center' }, # Scenes 'scenes': [{ 'story': 'Welcome to our company. We create innovative solutions. Join us on this journey.', 'createSceneOnEndOfSentence': True, 'highlightKeywords': True, 'sceneTransition': 'fade' }] } ) data = response.json() print('Job ID:', data['data']['jobId']) ``` ```php PHP theme={null} 'complete_video_render', 'aspectRatio' => '16:9', 'saveProject' => true, 'webhook' => 'https://your-server.com/webhook', // Branding 'brandName' => 'My Brand', // Voice-over 'voiceOver' => [ 'enabled' => true, 'aiVoices' => [[ 'speaker' => 'Emma', 'speed' => 100, 'amplificationLevel' => 0 ]] ], // Background music 'backgroundMusic' => [ 'enabled' => true, 'autoMusic' => true, 'volume' => 0.3 ], // Logo 'logo' => [ 'url' => 'https://example.com/logo.png', 'position' => 'top-right', 'width' => '15%' ], // Subtitle styling 'subtitleStyle' => [ 'fontFamily' => 'Montserrat', 'fontSize' => 48, 'color' => 'rgba(255,255,255,1)', 'position' => 'bottom-center' ], // Scenes 'scenes' => [[ 'story' => 'Welcome to our company. We create innovative solutions. Join us on this journey.', 'createSceneOnEndOfSentence' => true, 'highlightKeywords' => true, 'sceneTransition' => 'fade' ]] ]; $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Authorization: YOUR_API_KEY', 'Content-Type: application/json' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); echo 'Job ID: ' . $data['data']['jobId'] . PHP_EOL; ?> ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { url := "https://api.pictory.ai/pictoryapis/v2/video/storyboard/render" payload := map[string]interface{}{ "videoName": "complete_video_render", "aspectRatio": "16:9", "saveProject": true, "webhook": "https://your-server.com/webhook", // Branding "brandName": "My Brand", // Voice-over "voiceOver": map[string]interface{}{ "enabled": true, "aiVoices": []map[string]interface{}{ { "speaker": "Emma", "speed": 100, "amplificationLevel": 0, }, }, }, // Background music "backgroundMusic": map[string]interface{}{ "enabled": true, "autoMusic": true, "volume": 0.3, }, // Logo "logo": map[string]interface{}{ "url": "https://example.com/logo.png", "position": "top-right", "width": "15%", }, // Subtitle styling "subtitleStyle": map[string]interface{}{ "fontFamily": "Montserrat", "fontSize": 48, "color": "rgba(255,255,255,1)", "position": "bottom-center", }, // Scenes "scenes": []map[string]interface{}{ { "story": "Welcome to our company. We create innovative solutions. Join us on this journey.", "createSceneOnEndOfSentence": true, "highlightKeywords": true, "sceneTransition": "fade", }, }, } requestBody, _ := json.Marshal(payload) req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Job ID:", data["jobId"]) } ``` ### Poll for Completion ```javascript JavaScript theme={null} async function waitForVideo(jobId, apiKey) { const maxAttempts = 60; const pollInterval = 10000; // 10 seconds for (let attempt = 0; attempt < maxAttempts; attempt++) { const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`, { headers: { 'Authorization': apiKey } } ); const data = await response.json(); const status = data.data.status; console.log(`Attempt ${attempt + 1}: Status = ${status}`); if (status === 'completed') { console.log('Video URL:', data.data.videoURL); return data.data; } if (status === 'failed') { throw new Error('Video rendering failed: ' + JSON.stringify(data)); } await new Promise(resolve => setTimeout(resolve, pollInterval)); } throw new Error('Timeout waiting for video to render'); } // Usage const jobId = 'your-job-id'; const result = await waitForVideo(jobId, 'YOUR_API_KEY'); ``` ```python Python theme={null} import requests import time def wait_for_video(job_id, api_key, max_attempts=60, poll_interval=10): for attempt in range(max_attempts): response = requests.get( f'https://api.pictory.ai/pictoryapis/v1/jobs/{job_id}', headers={'Authorization': api_key} ) data = response.json() status = data['data']['status'] print(f'Attempt {attempt + 1}: Status = {status}') if status == 'completed': print('Video URL:', data['data']['videoURL']) return data['data'] if status == 'failed': raise Exception(f'Video rendering failed: {data}') time.sleep(poll_interval) raise Exception('Timeout waiting for video to render') # Usage job_id = 'your-job-id' result = wait_for_video(job_id, 'YOUR_API_KEY') ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "time" ) type JobResponse struct { Data struct { Status string `json:"status"` VideoURL string `json:"videoURL"` } `json:"data"` } func waitForVideo(jobID, apiKey string, maxAttempts int, pollInterval time.Duration) (*JobResponse, error) { for attempt := 0; attempt < maxAttempts; attempt++ { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/jobs/%s", jobID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } body, _ := io.ReadAll(resp.Body) resp.Body.Close() var result JobResponse json.Unmarshal(body, &result) status := result.Data.Status fmt.Printf("Attempt %d: Status = %s\n", attempt+1, status) if status == "completed" { fmt.Printf("Video URL: %s\n", result.Data.VideoURL) return &result, nil } if status == "failed" { return nil, fmt.Errorf("video rendering failed: %s", string(body)) } time.Sleep(pollInterval) } return nil, fmt.Errorf("timeout waiting for video to render") } func main() { jobID := "your-job-id" result, err := waitForVideo(jobID, "YOUR_API_KEY", 60, 10*time.Second) if err != nil { panic(err) } fmt.Printf("Video ready: %s\n", result.Data.VideoURL) } ``` *** ## Related APIs Preview storyboard before rendering Monitor rendering progress List saved video projects List available AI voices # Render Video with Modifications Source: https://docs.pictory.ai/api-reference/videos/render-video POST https://api.pictory.ai/pictoryapis/v2/video/render Render final video from modified storyboard elements returned in the preview response ## Overview The Render Video API generates a final video from storyboard elements that can be modified before rendering. This endpoint uses the `renderParams` property returned from a completed [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) job, allowing you to make changes to scenes, visuals, text, and other elements before producing the final video. This endpoint requires the `renderParams` object from a completed preview job. First create a preview, retrieve the job output to get `renderParams`, modify as needed, then submit to this endpoint. **Use this endpoint when you need to modify the preview** - change scenes, update visuals, adjust timing, or customize content before final render. *** ## Render Workflow Options | Workflow | API | When to Use | | ----------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | | **Create Preview** | [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) | Generate preview to review scenes before rendering | | **Render from Preview** | [Render from Preview](/api-reference/videos/render-from-preview) | Render preview as-is without modifications | | **Render with Modifications** | **This API** | Modify preview elements before rendering | | **Render Saved Project** | [Render Project](/api-reference/videos/render-project) | Render existing project created in App or via API | | **Direct Render** | [Render Storyboard Video](/api-reference/videos/render-storyboard-video) | Skip preview, render directly from input | ### When to Use This Endpoint Add, remove, or reorder scenes from the preview Replace AI-selected backgrounds with custom visuals Modify subtitle text or scene content Change scene durations or transitions *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v2/video/render ``` *** ## Request Parameters ### Headers API key for authentication ``` Authorization: YOUR_API_KEY ``` Must be `application/json` *** ## Request Body The request body should contain the values from the `renderParams` object returned in a completed preview job response. Send the renderParams content directly as the request body (not wrapped in a renderParams property). You can modify any properties before submitting. ### Getting renderParams from Preview Job After creating a storyboard preview, retrieve the job output using the [Get Storyboard Preview Job by ID](/api-reference/jobs/get-storyboard-preview-job-by-id) API. The response includes a `renderParams` object containing the complete storyboard configuration: ```json theme={null} { "job_id": "2cc183fc-f07a-4bf1-b7a0-92ec999b22f3", "success": true, "data": { "status": "completed", "renderParams": { "output": { "width": 1920, "height": 1080, "format": "mp4", "name": "my_video.mp4", "title": "my_video", "description": "" }, "elements": [ { "type": "audio", "id": "voiceOver", "elementType": "audioElement", "url": "https://...", "segments": [...] }, { "type": "audio", "id": "bgMusic", "elementType": "audioElement", "url": "https://...", "segments": [...] }, { "id": "backgroundElement_1", "elementType": "backgroundElement", "type": "video", "url": "https://...", "visualUrl": "https://...", "startTime": 0, "duration": 10.53 }, { "id": "SceneText_1_1", "elementType": "SceneText", "type": "text", "text": "Your subtitle text here...", "fontFamily": "Arial", "fontSize": "24", "startTime": 0, "duration": 10.53 } ], "sceneMarkers": [...], "subtitles": [...], "projectId": "1767054556293" }, "previewUrl": "https://video.pictory.ai/v2/preview/...", "projectUrl": "https://app.pictory.ai/v2/storyboard/...", "projectId": "1767054556293" } } ``` Use the contents of `renderParams` directly as the request body for this API: ```json theme={null} { "output": { "width": 1920, "height": 1080, "format": "mp4", "name": "my_video.mp4", "title": "my_video" }, "elements": [...], "sceneMarkers": [...], "subtitles": [...], "projectId": "1767054556293" } ``` ### Modifiable Elements Within `renderParams`, you can modify: | Element | Description | | ------------------------------------------------ | ------------------------------------------------------------- | | `output` | Video output settings (width, height, format, name, title) | | `elements` | Array of all video elements - audio, backgrounds, and text | | `elements[].url` | Change background video/image URL for backgroundElement types | | `elements[].text` | Update subtitle text for SceneText elements | | `elements[].duration` | Adjust element timing | | `elements[].fontFamily`, `fontSize`, `fontColor` | Modify text styling | | `sceneMarkers` | Scene timing and markers | | `subtitles` | Subtitle text content | ### Element Types | elementType | Description | | ------------------- | ------------------------------------ | | `audioElement` | Voice-over or background music audio | | `backgroundElement` | Scene background video or image | | `SceneText` | Subtitle/caption text overlay | *** ## Response When the render request is successfully submitted, a job is created and a job ID is returned. Indicates whether the request was successful Unique identifier for the render job. Use this to track the job status and retrieve results via the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint. ```json 200 - Job Created theme={null} { "success": true, "data": { "jobId": "265a7c1a-4985-4058-9208-68114f131a2b" } } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ## Next Steps Once you have the `jobId`, poll the [Get Video Render Job by ID](/api-reference/jobs/get-video-render-job-by-id) endpoint to check the render status and retrieve the output URLs when complete. Use a polling interval of 10–30 seconds. *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key. ### Basic Render with Modified Elements ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/video/render \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "output": { "width": 1920, "height": 1080, "format": "mp4", "name": "modified_video.mp4", "title": "modified_video" }, "elements": [...], "sceneMarkers": [...], "subtitles": [...], "projectId": "1767054556293" }' | python -m json.tool ``` ```javascript JavaScript theme={null} // Assume renderParams was retrieved from preview job response const renderParams = previewJobResponse.data.renderParams; // Modify as needed - change output title renderParams.output.title = 'modified_video'; renderParams.output.name = 'modified_video.mp4'; // Send renderParams directly as the request body const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(renderParams) }); const data = await response.json(); console.log('Render Job ID:', data.data.jobId); ``` ```python Python theme={null} import requests # Assume render_params was retrieved from preview job response render_params = preview_job_response['data']['renderParams'] # Modify as needed - change output title render_params['output']['title'] = 'modified_video' render_params['output']['name'] = 'modified_video.mp4' # Send render_params directly as the request body response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/render', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=render_params ) data = response.json() print('Render Job ID:', data['data']['jobId']) ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) func main() { // Assume renderParams was retrieved from preview job response var renderParams map[string]interface{} // renderParams = previewJobResponse["data"].(map[string]interface{})["renderParams"] // Modify as needed - change output title output := renderParams["output"].(map[string]interface{}) output["title"] = "modified_video" output["name"] = "modified_video.mp4" // Send renderParams directly as the request body url := "https://api.pictory.ai/pictoryapis/v2/video/render" requestBody, _ := json.Marshal(renderParams) req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) if err != nil { panic(err) } req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) data := result["data"].(map[string]interface{}) fmt.Println("Render Job ID:", data["jobId"]) } ``` ### Complete Workflow: Preview, Modify, Render ```javascript JavaScript theme={null} const API_KEY = 'YOUR_API_KEY'; const API_BASE = 'https://api.pictory.ai/pictoryapis'; // Step 1: Create storyboard preview async function createPreview() { const response = await fetch(`${API_BASE}/v2/video/storyboard`, { method: 'POST', headers: { 'Authorization': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_video', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story: 'Welcome to our product demo. We will show you amazing features.', createSceneOnEndOfSentence: true }] }) }); const data = await response.json(); return data.data.jobId; } // Step 2: Wait for job and get renderParams async function getJobResult(jobId) { while (true) { const response = await fetch(`${API_BASE}/v1/jobs/${jobId}`, { headers: { 'Authorization': API_KEY } }); const data = await response.json(); if (data.data.status === 'completed') { return data.data; } if (data.data.status === 'failed') { throw new Error('Job failed'); } await new Promise(resolve => setTimeout(resolve, 5000)); } } // Step 3: Modify renderParams and render async function renderWithModifications(renderParams) { // Example modifications: // Change output title renderParams.output.title = 'modified_video'; renderParams.output.name = 'modified_video.mp4'; // Modify a background element's URL (find backgroundElement by id) // const bgElement = renderParams.elements.find(el => el.id === 'backgroundElement_1'); // if (bgElement) bgElement.url = 'https://example.com/new-bg.mp4'; // Submit renderParams directly as the request body const response = await fetch(`${API_BASE}/v2/video/render`, { method: 'POST', headers: { 'Authorization': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify(renderParams) }); const data = await response.json(); return data.data.jobId; } // Execute workflow async function main() { try { // Create preview console.log('Creating preview...'); const previewJobId = await createPreview(); console.log('Preview Job ID:', previewJobId); // Wait for preview and get renderParams console.log('Waiting for preview...'); const previewResult = await getJobResult(previewJobId); console.log('Preview completed!'); const renderParams = previewResult.renderParams; // Modify and render console.log('Rendering with modifications...'); const renderJobId = await renderWithModifications(renderParams); console.log('Render Job ID:', renderJobId); // Wait for render const renderResult = await getJobResult(renderJobId); console.log('Video URL:', renderResult.videoURL); } catch (error) { console.error('Error:', error.message); } } main(); ``` ```python Python theme={null} import requests import time API_KEY = 'YOUR_API_KEY' API_BASE = 'https://api.pictory.ai/pictoryapis' def create_preview(): """Step 1: Create storyboard preview""" response = requests.post( f'{API_BASE}/v2/video/storyboard', headers={ 'Authorization': API_KEY, 'Content-Type': 'application/json' }, json={ 'videoName': 'my_video', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [{ 'story': 'Welcome to our product demo. We will show you amazing features.', 'createSceneOnEndOfSentence': True }] } ) return response.json()['data']['jobId'] def get_job_result(job_id): """Step 2: Wait for job and get result""" while True: response = requests.get( f'{API_BASE}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) data = response.json() if data['data']['status'] == 'completed': return data['data'] if data['data']['status'] == 'failed': raise Exception('Job failed') time.sleep(5) def render_with_modifications(render_params): """Step 3: Modify renderParams and render""" # Example modifications: # Change output title render_params['output']['title'] = 'modified_video' render_params['output']['name'] = 'modified_video.mp4' # Modify a background element's URL (find backgroundElement by id) # for el in render_params['elements']: # if el.get('id') == 'backgroundElement_1': # el['url'] = 'https://example.com/new-bg.mp4' # Submit render_params directly as the request body response = requests.post( f'{API_BASE}/v2/video/render', headers={ 'Authorization': API_KEY, 'Content-Type': 'application/json' }, json=render_params ) return response.json()['data']['jobId'] def main(): try: # Create preview print('Creating preview...') preview_job_id = create_preview() print(f'Preview Job ID: {preview_job_id}') # Wait for preview and get renderParams print('Waiting for preview...') preview_result = get_job_result(preview_job_id) print('Preview completed!') render_params = preview_result['renderParams'] # Modify and render print('Rendering with modifications...') render_job_id = render_with_modifications(render_params) print(f'Render Job ID: {render_job_id}') # Wait for render render_result = get_job_result(render_job_id) print(f'Video URL: {render_result["videoURL"]}') except Exception as e: print(f'Error: {e}') if __name__ == '__main__': main() ``` ### Example: Replace Scene Background ```javascript JavaScript theme={null} // Get renderParams from preview job const renderParams = previewJobResponse.data.renderParams; // Find and replace background for first scene (backgroundElement_1) const bgElement = renderParams.elements.find(el => el.id === 'backgroundElement_1'); if (bgElement) { bgElement.url = 'https://your-domain.com/custom-background.mp4'; bgElement.visualUrl = 'https://your-domain.com/custom-background.mp4'; } // Render with new background - send renderParams directly const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/render', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify(renderParams) }); ``` ```python Python theme={null} # Get render_params from preview job render_params = preview_job_response['data']['renderParams'] # Find and replace background for first scene (backgroundElement_1) for element in render_params['elements']: if element.get('id') == 'backgroundElement_1': element['url'] = 'https://your-domain.com/custom-background.mp4' element['visualUrl'] = 'https://your-domain.com/custom-background.mp4' break # Render with new background - send render_params directly response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/render', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json=render_params ) ``` ```php PHP theme={null} ``` ```go Go theme={null} package main import ( "bytes" "encoding/json" "net/http" ) func main() { // Get renderParams from preview job var renderParams map[string]interface{} // renderParams = previewJobResponse["data"].(map[string]interface{})["renderParams"] // Find and replace background for first scene (backgroundElement_1) elements := renderParams["elements"].([]interface{}) for _, el := range elements { element := el.(map[string]interface{}) if element["id"] == "backgroundElement_1" { element["url"] = "https://your-domain.com/custom-background.mp4" element["visualUrl"] = "https://your-domain.com/custom-background.mp4" break } } // Render with new background - send renderParams directly url := "https://api.pictory.ai/pictoryapis/v2/video/render" requestBody, _ := json.Marshal(renderParams) req, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) req.Header.Set("Authorization", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} client.Do(req) } ``` *** ## Error Handling ```json theme={null} { "success": false, "error": { "code": "INVALID_REQUEST", "message": "elements is required" } } ``` **Solution:** Ensure your request body contains all required fields from the preview job's `renderParams` including `output`, `elements`, `sceneMarkers`, and `projectId`. Send the renderParams content directly as the request body. ```json theme={null} { "success": false, "error": { "code": "INVALID_REQUEST", "message": "Invalid request structure" } } ``` **Solution:** Ensure the request body maintains the required structure from the preview job's `renderParams`. Only modify specific properties, do not remove required fields. ```json theme={null} { "success": false, "error": { "message": "Invalid element configuration at index 0" } } ``` **Solution:** Check that modified elements have valid configurations. Ensure URLs are accessible and properly formatted. Each element must have valid `id`, `elementType`, and `type` properties. ```json theme={null} { "message": "Unauthorized" } ``` **Solution:** Check your API key is valid and correctly formatted in the Authorization header. *** ## Related APIs Generate preview to get renderParams (Step 1) Render preview as-is without modifications Monitor rendering progress Find replacement visuals for scenes # Create Vimeo Connection Source: https://docs.pictory.ai/api-reference/vimeo-integration/create-vimeo-connection POST https://api.pictory.ai/pictoryapis/v1/vimeo-connections Connect your Vimeo account to enable automatic video uploads ## Overview Create a new Vimeo connection using your authentication credentials and configuration. Each connection name must be unique within your account. For security, sensitive credentials (client secret and access token) are securely stored and never returned in subsequent API responses. You need a valid API key and Vimeo developer credentials to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/vimeo-connections ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Body Parameters Display name for the Vimeo connection. Must be unique within your account and can only contain letters, numbers, spaces, underscores, and hyphens. **Maximum length:** 100 characters **Example:** `"My Vimeo Account"` Optional description explaining the connection's purpose or usage. **Maximum length:** 250 characters **Example:** `"Primary Vimeo account for marketing videos"` Whether the connection should be active immediately after creation. Set to `true` to enable, or `false` to create in a disabled state. **Example:** `true` Vimeo application Client ID from your [Vimeo app settings](https://developer.vimeo.com/apps). This identifies your application to Vimeo's API. **Maximum length:** 500 characters **Example:** `"abc123def456ghi789jkl012"` Vimeo application client secret from your app settings. Keep this confidential and never expose it in client-side code. **Maximum length:** 500 characters **Example:** `"xyz789abc123def456ghi789"` Vimeo access token that grants your application permission to access Vimeo resources. Obtain this through Vimeo's OAuth flow or from your app settings. **Maximum length:** 500 characters **Example:** `"1234567890abcdef1234567890abcdef"` *** ## Response Returns the created Vimeo connection object including the `connectionId`, `name`, `description`, `enabled` status, and `version` number. For security, sensitive credentials (`clientSecret` and `accessToken`) are never returned in API responses - you will only see the `clientIdentifier`. ### Response Examples ```json 201 - Success theme={null} { "name": "Test Vimeo Connection", "description": "Testing Vimeo connection via API", "enabled": true, "clientIdentifier": "test_client_id_123", "connectionId": "20251222155613307xv0nodhitf9cd0f", "version": 1 } ``` ```json 400 - Duplicate Name theme={null} { "code": "DUPLICATE_CONNECTION", "message": "Connection with same name already exist.", "fields": [] } ``` ```json 400 - Validation Error theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "name", "errors": "name is required" } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Create a new Vimeo connection # Replace YOUR_API_KEY with your actual API key curl --request POST \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "My Vimeo Account", "description": "Primary Vimeo account for marketing videos", "enabled": true, "clientIdentifier": "abc123def456ghi789jkl012", "clientSecret": "xyz789abc123def456ghi789", "accessToken": "1234567890abcdef1234567890abcdef" }' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const createVimeoConnection = async (apiKey, connectionData) => { const response = await fetch('https://api.pictory.ai/pictoryapis/v1/vimeo-connections', { method: 'POST', headers: { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(connectionData) }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const connectionData = { name: 'My Vimeo Account', description: 'Primary Vimeo account for marketing videos', enabled: true, clientIdentifier: 'abc123def456ghi789jkl012', clientSecret: 'xyz789abc123def456ghi789', accessToken: '1234567890abcdef1234567890abcdef' }; const result = await createVimeoConnection('YOUR_API_KEY', connectionData); console.log('Connection Created:', result); console.log('Connection ID:', result.connectionId); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def create_vimeo_connection(api_key, connection_data): """Create a new Vimeo connection""" url = "https://api.pictory.ai/pictoryapis/v1/vimeo-connections" headers = { "Authorization": api_key, "Content-Type": "application/json" } response = requests.post(url, headers=headers, json=connection_data) response.raise_for_status() return response.json() # Usage try: connection_data = { "name": "My Vimeo Account", "description": "Primary Vimeo account for marketing videos", "enabled": True, "clientIdentifier": "abc123def456ghi789jkl012", "clientSecret": "xyz789abc123def456ghi789", "accessToken": "1234567890abcdef1234567890abcdef" } result = create_vimeo_connection("YOUR_API_KEY", connection_data) print(f"Connection Created: {result}") print(f"Connection ID: {result['connectionId']}") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} 'My Vimeo Account', 'description' => 'Primary Vimeo account for marketing videos', 'enabled' => true, 'clientIdentifier' => 'abc123def456ghi789jkl012', 'clientSecret' => 'xyz789abc123def456ghi789', 'accessToken' => '1234567890abcdef1234567890abcdef' ]; $result = createVimeoConnection('YOUR_API_KEY', $connectionData); echo "Connection Created:\n"; print_r($result); echo "Connection ID: " . $result['connectionId'] . "\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type VimeoConnectionRequest struct { Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` ClientIdentifier string `json:"clientIdentifier"` ClientSecret string `json:"clientSecret"` AccessToken string `json:"accessToken"` } type VimeoConnectionResponse struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` ClientIdentifier string `json:"clientIdentifier"` Enabled bool `json:"enabled"` Version int `json:"version"` } func createVimeoConnection(apiKey string, connectionData VimeoConnectionRequest) (*VimeoConnectionResponse, error) { url := "https://api.pictory.ai/pictoryapis/v1/vimeo-connections" jsonData, err := json.Marshal(connectionData) if err != nil { return nil, err } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusCreated { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result VimeoConnectionResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { connectionData := VimeoConnectionRequest{ Name: "My Vimeo Account", Description: "Primary Vimeo account for marketing videos", Enabled: true, ClientIdentifier: "abc123def456ghi789jkl012", ClientSecret: "xyz789abc123def456ghi789", AccessToken: "1234567890abcdef1234567890abcdef", } result, err := createVimeoConnection("YOUR_API_KEY", connectionData) if err != nil { panic(err) } fmt.Printf("Connection Created: %+v\n", result) fmt.Printf("Connection ID: %s\n", result.ConnectionID) } ``` *** ## Error Handling **Cause:** A connection with the same name already exists in your account **Solution:** * Choose a unique name for your connection * Use the [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) endpoint to see existing connection names * Or update the existing connection using the [Update Vimeo Connection](/api-reference/vimeo-integration/update-vimeo-connection) endpoint **Cause:** Required fields are missing or field values do not meet validation requirements **Solution:** * Ensure all required fields are included: `name`, `enabled`, `clientIdentifier`, `clientSecret`, `accessToken` * Verify field values do not exceed maximum lengths: * `name`: 100 characters max * `description`: 250 characters max * `clientIdentifier`, `clientSecret`, `accessToken`: 500 characters max * Check that `name` only contains letters, numbers, spaces, underscores, and hyphens * Verify the request body is valid JSON **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** The Vimeo client identifier, client secret, or access token is invalid **Solution:** * Verify your credentials in the [Vimeo Developer Portal](https://developer.vimeo.com/apps) * Ensure you are using the correct Client ID, Client Secret, and Access Token * Check that your Vimeo app has the necessary permissions (video upload and management) * Generate a new access token if the current one has expired *** ## Prerequisites: Getting Vimeo Credentials Before creating a Vimeo connection, you need to obtain credentials from Vimeo: 1. Go to the [Vimeo Developer Portal](https://developer.vimeo.com/apps) 2. Sign in with your Vimeo account 3. Click **Create App** or **New App** 4. Fill in the required details (app name, description, etc.) 5. Click **Create App** 1. Once your app is created, you will see the app details page 2. Note down the **Client Identifier** (Client ID) 3. Note down the **Client Secret** (keep this secure!) 4. Generate an **Access Token** with the required scopes: * `video` - Upload and manage videos * `private` - Access private videos Make sure your Vimeo app has the necessary permissions enabled: * Video upload * Video management * Account access **Keep Your Credentials Secure:** Never expose your Client Secret or Access Token in client-side code or public repositories. These credentials provide full access to your Vimeo account. # Delete Vimeo Connection Source: https://docs.pictory.ai/api-reference/vimeo-integration/delete-vimeo-connection DELETE https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} Permanently delete a Vimeo connection ## Overview Permanently delete a Vimeo connection and all its associated configuration data. This action cannot be undone. After deletion, any integrations or workflows using this connection will no longer function. Only the connection owner can delete it. **Permanent Action:** Deleting a connection is irreversible. Ensure you no longer need this connection before proceeding. Consider disabling it instead if you might need it again later. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} DELETE https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Path Parameters The unique identifier of the Vimeo connection to delete **Example:** `"20251222155613307xv0nodhitf9cd0f"` *** ## Response Returns HTTP 204 (No Content) on successful deletion. The response body is empty as the connection no longer exists. This endpoint is idempotent - deleting an already-deleted or non-existent connection also returns 204. If authentication fails, an error response is returned with details. ### Response Examples ```json 204 - Success theme={null} No content returned. The connection has been successfully deleted. ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Delete a Vimeo connection # Replace YOUR_API_KEY with your actual API key # Replace CONNECTION_ID with the actual connection ID curl --request DELETE \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections/CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const deleteVimeoConnection = async (apiKey, connectionId) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/vimeo-connections/${connectionId}`, { method: 'DELETE', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } // 204 responses have no content return { success: true, message: 'Connection deleted successfully' }; }; // Usage try { const connectionId = '20251222155613307xv0nodhitf9cd0f'; const result = await deleteVimeoConnection('YOUR_API_KEY', connectionId); console.log(result.message); console.log(`Connection ${connectionId} has been permanently deleted`); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def delete_vimeo_connection(api_key, connection_id): """Delete a Vimeo connection""" url = f"https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connection_id}" headers = { "Authorization": api_key } response = requests.delete(url, headers=headers) response.raise_for_status() # 204 responses have no content return {"success": True, "message": "Connection deleted successfully"} # Usage try: connection_id = "20251222155613307xv0nodhitf9cd0f" result = delete_vimeo_connection("YOUR_API_KEY", connection_id) print(result["message"]) print(f"Connection {connection_id} has been permanently deleted") except requests.exceptions.HTTPError as e: if e.response.status_code == 404: print("Error: Connection not found or already deleted") else: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} true, 'message' => 'Connection deleted successfully' ]; } // Usage try { $connectionId = '20251222155613307xv0nodhitf9cd0f'; $result = deleteVimeoConnection('YOUR_API_KEY', $connectionId); echo $result['message'] . "\n"; echo "Connection {$connectionId} has been permanently deleted\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "fmt" "io" "net/http" ) func deleteVimeoConnection(apiKey string, connectionId string) error { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/vimeo-connections/%s", connectionId) req, err := http.NewRequest("DELETE", url, nil) if err != nil { return err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } // 204 responses have no content return nil } // Usage func main() { connectionId := "20251222155613307xv0nodhitf9cd0f" err := deleteVimeoConnection("YOUR_API_KEY", connectionId) if err != nil { panic(err) } fmt.Println("Connection deleted successfully") fmt.Printf("Connection %s has been permanently deleted\n", connectionId) } ``` *** ## Error Handling **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Behavior:** This endpoint is idempotent and always returns 204 (No Content) on success **What This Means:** * Deleting an already-deleted connection returns 204 (success) * Deleting a non-existent connection returns 204 (success) * Multiple delete requests for the same connection are safe * You can delete without checking if the connection exists first * This follows REST API best practices for DELETE operations *** ## Best Practices ### Before Deleting a Connection 1. **Verify Dependencies:** Check if any active workflows or integrations are using this connection 2. **Export Configuration:** Note down the connection settings if you might need to recreate it later 3. **Consider Disabling:** If you are unsure, use the [Update Vimeo Connection](/api-reference/vimeo-integration/update-vimeo-connection) endpoint to set `enabled: false` instead of deleting 4. **Confirm Identity:** Double-check the connection ID to ensure you are deleting the correct connection **Idempotent Operation:** This endpoint is safe to call multiple times. If you are unsure whether a connection exists, you can simply delete it without checking first - the operation will succeed either way. ### Alternative to Deletion Instead of permanently deleting a connection, you can disable it: ```json theme={null} PUT /v1/vimeo-connections/{connectionid} { "enabled": false, "version": 1 } ``` Disabling preserves all configuration data while preventing the connection from being used. You can re-enable it later if needed. ### Safe Deletion Workflow ```javascript theme={null} // Example: Safe deletion workflow with confirmation const safeDeleteConnection = async (apiKey, connectionId) => { // Step 1: Get connection details const connection = await getVimeoConnectionById(apiKey, connectionId); console.log(`About to delete: ${connection.name}`); // Step 2: Confirm (in production, get user confirmation) const confirmed = true; // Replace with actual confirmation logic if (!confirmed) { return { cancelled: true }; } // Step 3: Delete the connection await deleteVimeoConnection(apiKey, connectionId); console.log('Connection deleted successfully'); return { success: true }; }; ``` **Impact on Videos:** Deleting a Vimeo connection does not affect videos that were previously uploaded using this connection. Those videos remain in your Vimeo account. However, you will not be able to upload new videos through this connection. # Get Vimeo Connection by ID Source: https://docs.pictory.ai/api-reference/vimeo-integration/get-vimeo-connection-by-id GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} Retrieve details of a specific Vimeo connection ## Overview Retrieve detailed information about a specific Vimeo connection using its unique identifier. For security, sensitive credentials (client secret and access token) are excluded from the response. The connection must belong to your account. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Path Parameters The unique identifier of the Vimeo connection to retrieve **Example:** `"20251222155613307xv0nodhitf9cd0f"` *** ## Response Returns the Vimeo connection object with all configuration details including `connectionId`, `name`, `description`, `clientIdentifier`, `type`, `enabled` status, `createdDate`, `updatedDate`, and `version` number. For security, sensitive credentials (`clientSecret` and `accessToken`) are never returned in API responses. ### Response Examples ```json 200 - Success theme={null} { "connectionId": "20251222155613307xv0nodhitf9cd0f", "name": "Test Vimeo Connection", "description": "Testing Vimeo connection via API", "clientIdentifier": "test_client_id_123", "type": "VIMEO", "enabled": true, "createdDate": "2025-12-22T15:56:12.309Z", "updatedDate": "2025-12-22T15:56:12.309Z", "version": 1 } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Connection not found" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Retrieve a specific Vimeo connection by ID # Replace YOUR_API_KEY with your actual API key # Replace CONNECTION_ID with the actual connection ID curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections/CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const getVimeoConnectionById = async (apiKey, connectionId) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/vimeo-connections/${connectionId}`, { method: 'GET', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const connectionId = '20251222155613307xv0nodhitf9cd0f'; const connection = await getVimeoConnectionById('YOUR_API_KEY', connectionId); console.log('Connection Details:', connection); console.log('Connection Name:', connection.name); console.log('Is Enabled:', connection.enabled); console.log('Version:', connection.version); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def get_vimeo_connection_by_id(api_key, connection_id): """Retrieve a specific Vimeo connection by ID""" url = f"https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connection_id}" headers = { "Authorization": api_key } response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # Usage try: connection_id = "20251222155613307xv0nodhitf9cd0f" connection = get_vimeo_connection_by_id("YOUR_API_KEY", connection_id) print(f"Connection Details: {connection}") print(f"Connection Name: {connection['name']}") print(f"Is Enabled: {connection['enabled']}") print(f"Version: {connection['version']}") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type VimeoConnection struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` ClientIdentifier string `json:"clientIdentifier"` Type string `json:"type"` Enabled bool `json:"enabled"` CreatedDate string `json:"createdDate"` UpdatedDate string `json:"updatedDate"` Version int `json:"version"` } func getVimeoConnectionById(apiKey string, connectionId string) (*VimeoConnection, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/vimeo-connections/%s", connectionId) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result VimeoConnection if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { connectionId := "20251222155613307xv0nodhitf9cd0f" connection, err := getVimeoConnectionById("YOUR_API_KEY", connectionId) if err != nil { panic(err) } fmt.Printf("Connection Details: %+v\n", connection) fmt.Printf("Connection Name: %s\n", connection.Name) fmt.Printf("Is Enabled: %t\n", connection.Enabled) fmt.Printf("Version: %d\n", connection.Version) } ``` *** ## Error Handling **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** The connection ID does not exist or you do not have access to it **Solution:** * Verify the connection ID is correct and complete * Ensure the connection belongs to your account * Check if the connection has been deleted * Connection IDs are case-sensitive - confirm the exact casing * Use the [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) endpoint to list all your connections **Cause:** The returned connection details do not match expectations **Solution:** * Double-check the connection ID in the URL path * Verify you are using the correct API key for the account that owns the connection * Ensure you have not confused this connection with another one * Check if the connection was recently updated by someone else # Get Vimeo Connections Source: https://docs.pictory.ai/api-reference/vimeo-integration/get-vimeo-connections GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections Retrieve all configured Vimeo connections ## Overview Retrieve a paginated list of all Vimeo connections associated with your account. The response includes connection metadata while excluding sensitive credentials (client secrets and access tokens) for security. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Query Parameters Base64-encoded pagination token for retrieving the next page of results. Returned in the previous response when more results are available. **Example:** `"eyJsYXN0RXZhbHVhdGVkS2V5Ijp7fX0="` *** ## Response Returns a paginated array of Vimeo connection objects in the `Items` field. Each connection includes `connectionId`, `name`, `description`, `clientIdentifier`, `type`, `enabled` status, timestamps, and `version` number. If more results are available, a `nextPageKey` is included for pagination. For security, sensitive credentials (`clientSecret` and `accessToken`) are never returned in API responses. ### Response Examples ```json 200 - Success theme={null} { "Items": [ { "connectionId": "20251004004125558w7qnfweid5ilsos", "name": "Pictory API", "clientIdentifier": "925dbcbdc591d6463c5e289a3825e1a3e0602b97", "type": "VIMEO", "enabled": true, "createdDate": "2025-10-04T00:41:24.597Z", "updatedDate": "2025-10-04T00:41:24.597Z", "version": 1 }, { "connectionId": "20251222155613307xv0nodhitf9cd0f", "name": "Test Vimeo Connection", "description": "Testing Vimeo connection via API", "clientIdentifier": "test_client_id_123", "type": "VIMEO", "enabled": true, "createdDate": "2025-12-22T15:56:12.309Z", "updatedDate": "2025-12-22T15:56:12.309Z", "version": 1 } ] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Retrieve all Vimeo connections # Replace YOUR_API_KEY with your actual API key curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections \ --header 'Authorization: YOUR_API_KEY' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const getVimeoConnections = async (apiKey, nextPageKey = null) => { const url = new URL('https://api.pictory.ai/pictoryapis/v1/vimeo-connections'); // Add pagination parameter if provided if (nextPageKey) { url.searchParams.append('nextPageKey', nextPageKey); } const response = await fetch(url, { method: 'GET', headers: { 'Authorization': `${apiKey}` } }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage try { const result = await getVimeoConnections('YOUR_API_KEY'); console.log('Vimeo Connections:', result.Items); console.log('Total Connections:', result.Items.length); // Handle pagination if nextPageKey exists if (result.nextPageKey) { console.log('More results available. Use nextPageKey for next page.'); const nextPage = await getVimeoConnections('YOUR_API_KEY', result.nextPageKey); console.log('Next Page:', nextPage.Items); } } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def get_vimeo_connections(api_key, next_page_key=None): """Retrieve all Vimeo connections""" url = "https://api.pictory.ai/pictoryapis/v1/vimeo-connections" headers = { "Authorization": api_key } # Add pagination parameter if provided params = {} if next_page_key: params['nextPageKey'] = next_page_key response = requests.get(url, headers=headers, params=params) response.raise_for_status() return response.json() # Usage try: result = get_vimeo_connections("YOUR_API_KEY") print(f"Vimeo Connections: {result['Items']}") print(f"Total Connections: {len(result['Items'])}") # Handle pagination if nextPageKey exists if 'nextPageKey' in result: print("More results available. Use nextPageKey for next page.") next_page = get_vimeo_connections("YOUR_API_KEY", result['nextPageKey']) print(f"Next Page: {next_page['Items']}") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" ) type VimeoConnection struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` ClientIdentifier string `json:"clientIdentifier"` Type string `json:"type"` Enabled bool `json:"enabled"` CreatedDate string `json:"createdDate"` UpdatedDate string `json:"updatedDate"` Version int `json:"version"` } type VimeoConnectionsResponse struct { Items []VimeoConnection `json:"Items"` NextPageKey string `json:"nextPageKey,omitempty"` } func getVimeoConnections(apiKey string, nextPageKey string) (*VimeoConnectionsResponse, error) { baseURL := "https://api.pictory.ai/pictoryapis/v1/vimeo-connections" // Add pagination parameter if provided if nextPageKey != "" { params := url.Values{} params.Add("nextPageKey", nextPageKey) baseURL = baseURL + "?" + params.Encode() } req, err := http.NewRequest("GET", baseURL, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result VimeoConnectionsResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { result, err := getVimeoConnections("YOUR_API_KEY", "") if err != nil { panic(err) } fmt.Printf("Vimeo Connections: %+v\n", result.Items) fmt.Printf("Total Connections: %d\n", len(result.Items)) // Handle pagination if nextPageKey exists if result.NextPageKey != "" { fmt.Println("More results available. Use nextPageKey for next page.") nextPage, err := getVimeoConnections("YOUR_API_KEY", result.NextPageKey) if err != nil { panic(err) } fmt.Printf("Next Page: %+v\n", nextPage.Items) } } ``` *** ## Pagination This endpoint supports pagination for handling large result sets: 1. **First Request:** Make initial request without the `nextPageKey` parameter 2. **Check for More Results:** If the response includes `nextPageKey`, additional results are available 3. **Subsequent Requests:** Include the `nextPageKey` value as a query parameter to retrieve the next page **Example with pagination:** ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections?nextPageKey=eyJsYXN0RXZhbHVhdGVkS2V5Ijp7fX0= ``` *** ## Error Handling **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** No Vimeo connections exist in your account **Solution:** * This is expected if you have not created any Vimeo connections yet * Use the [Create Vimeo Connection](/api-reference/vimeo-integration/create-vimeo-connection) endpoint to add your first connection * Verify you are using the correct API key for your account **Cause:** Getting the same results when using `nextPageKey` or pagination not working **Solution:** * Ensure you are passing the `nextPageKey` value exactly as returned (Base64-encoded) * Do not modify or decode the `nextPageKey` value before sending it * If no `nextPageKey` is returned in the response, you have reached the end of results * The `nextPageKey` is only valid for the current result set # Update Vimeo Connection Source: https://docs.pictory.ai/api-reference/vimeo-integration/update-vimeo-connection PUT https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} Update an existing Vimeo connection configuration ## Overview Update an existing Vimeo connection configuration including name, description, enabled status, and authentication credentials. The version number must be provided to prevent concurrent modification conflicts. When updating the connection name, it must remain unique within your account. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} PUT https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connectionid} ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` ### Path Parameters The unique identifier of the Vimeo connection to update **Example:** `"20251222155613307xv0nodhitf9cd0f"` ### Body Parameters Current version number of the connection. Must match the version in the database to prevent conflicts from concurrent updates. Update fails if the version does not match. **Example:** `1` Updated display name for the Vimeo connection. Must be unique within your account and can only contain letters, numbers, spaces, underscores, and hyphens. **Maximum length:** 100 characters **Example:** `"Updated Vimeo Account"` Updated description explaining the connection's purpose or usage. **Maximum length:** 250 characters **Example:** `"Updated description for marketing videos"` Whether the connection should be active. Set to `true` to enable, or `false` to disable. Disabling prevents usage but retains all configuration. **Example:** `false` Updated Vimeo application Client ID from your [Vimeo app settings](https://developer.vimeo.com/apps). Changes which Vimeo application this connection uses for authentication. **Maximum length:** 500 characters **Example:** `"updated_client_id_123"` Updated Vimeo application client secret from your app settings. Use this to rotate credentials. Keep this value secure. **Maximum length:** 500 characters **Example:** `"updated_secret_xyz789"` Updated Vimeo access token for API authentication. Use this to refresh or change the token when it expires or when changing permissions scope. **Maximum length:** 500 characters **Example:** `"updated_token_1234567890abcdef"` *** ## Response Returns the updated Vimeo connection object with all current configuration details. The `version` number is automatically incremented with each successful update for optimistic locking. The response includes `connectionId`, `name`, `description`, `clientIdentifier`, `type`, `enabled` status, and timestamps. For security, sensitive credentials (`clientSecret` and `accessToken`) are never returned in API responses - you will only see the `clientIdentifier`. ### Response Examples ```json 200 - Success theme={null} { "connectionId": "20251222155613307xv0nodhitf9cd0f", "name": "Updated Vimeo Connection Name", "description": "Updated description for testing PUT endpoint", "clientIdentifier": "updated_client_id_456", "type": "VIMEO", "enabled": false, "createdDate": "2025-12-22T15:56:12.309Z", "updatedDate": "2025-12-22T16:19:16.058Z", "version": 4 } ``` ```json 400 - Missing Version theme={null} { "code": "INVALID_REQUEST_BODY", "message": "Request body validation failed.", "fields": [ { "name": "version", "errors": "version is required" } ] } ``` ```json 400 - Duplicate Name theme={null} { "code": "DUPLICATE_CONNECTION", "message": "Connection with same name or account/region already exist.", "fields": [] } ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 404 - Not Found theme={null} { "message": "Connection not found" } ``` ```json 500 - Internal Server Error theme={null} { "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} # Update a Vimeo connection # Replace YOUR_API_KEY with your actual API key # Replace CONNECTION_ID with the actual connection ID # Partial update - update only description curl --request PUT \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections/CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "description": "Updated description", "version": 1 }' # Full update - update multiple fields curl --request PUT \ --url https://api.pictory.ai/pictoryapis/v1/vimeo-connections/CONNECTION_ID \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "name": "Updated Connection Name", "description": "Updated description for marketing videos", "enabled": false, "clientIdentifier": "new_client_id_456", "clientSecret": "new_secret_xyz789", "accessToken": "new_token_1234567890", "version": 1 }' | python -m json.tool ``` ```javascript JavaScript / Node.js theme={null} // Replace 'YOUR_API_KEY' with your actual API key const updateVimeoConnection = async (apiKey, connectionId, updates) => { const response = await fetch(`https://api.pictory.ai/pictoryapis/v1/vimeo-connections/${connectionId}`, { method: 'PUT', headers: { 'Authorization': `${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed: ${error.message}`); } return await response.json(); }; // Usage - Partial update try { const connectionId = '20251222155613307xv0nodhitf9cd0f'; // First, get current connection to retrieve version const getResponse = await fetch(`https://api.pictory.ai/pictoryapis/v1/vimeo-connections/${connectionId}`, { headers: { 'Authorization': 'YOUR_API_KEY' } }); const currentConnection = await getResponse.json(); // Partial update - only update description const partialUpdate = await updateVimeoConnection('YOUR_API_KEY', connectionId, { description: 'Updated description', version: currentConnection.version }); console.log('Partial Update Result:', partialUpdate); // Full update - update multiple fields const fullUpdate = await updateVimeoConnection('YOUR_API_KEY', connectionId, { name: 'Updated Connection Name', description: 'Updated description for marketing videos', enabled: false, clientIdentifier: 'new_client_id_456', clientSecret: 'new_secret_xyz789', accessToken: 'new_token_1234567890', version: partialUpdate.version // Use updated version }); console.log('Full Update Result:', fullUpdate); console.log('New Version:', fullUpdate.version); } catch (error) { console.error('Error:', error.message); } ``` ```python Python theme={null} # Replace 'YOUR_API_KEY' with your actual API key import requests def update_vimeo_connection(api_key, connection_id, updates): """Update a Vimeo connection""" url = f"https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connection_id}" headers = { "Authorization": api_key, "Content-Type": "application/json" } response = requests.put(url, headers=headers, json=updates) response.raise_for_status() return response.json() # Usage try: connection_id = "20251222155613307xv0nodhitf9cd0f" # First, get current connection to retrieve version get_url = f"https://api.pictory.ai/pictoryapis/v1/vimeo-connections/{connection_id}" get_response = requests.get(get_url, headers={"Authorization": "YOUR_API_KEY"}) current_connection = get_response.json() # Partial update - only update description partial_update = update_vimeo_connection("YOUR_API_KEY", connection_id, { "description": "Updated description", "version": current_connection["version"] }) print(f"Partial Update Result: {partial_update}") # Full update - update multiple fields full_update = update_vimeo_connection("YOUR_API_KEY", connection_id, { "name": "Updated Connection Name", "description": "Updated description for marketing videos", "enabled": False, "clientIdentifier": "new_client_id_456", "clientSecret": "new_secret_xyz789", "accessToken": "new_token_1234567890", "version": partial_update["version"] # Use updated version }) print(f"Full Update Result: {full_update}") print(f"New Version: {full_update['version']}") except requests.exceptions.HTTPError as e: error = e.response.json() print(f"Error: {error.get('message', 'Request failed')}") ``` ```php PHP theme={null} 'Updated description', 'version' => $currentConnection['version'] ]); echo "Partial Update Result:\n"; print_r($partialUpdate); // Full update - update multiple fields $fullUpdate = updateVimeoConnection('YOUR_API_KEY', $connectionId, [ 'name' => 'Updated Connection Name', 'description' => 'Updated description for marketing videos', 'enabled' => false, 'clientIdentifier' => 'new_client_id_456', 'clientSecret' => 'new_secret_xyz789', 'accessToken' => 'new_token_1234567890', 'version' => $partialUpdate['version'] // Use updated version ]); echo "Full Update Result:\n"; print_r($fullUpdate); echo "New Version: " . $fullUpdate['version'] . "\n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type VimeoConnectionUpdate struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Enabled *bool `json:"enabled,omitempty"` ClientIdentifier string `json:"clientIdentifier,omitempty"` ClientSecret string `json:"clientSecret,omitempty"` AccessToken string `json:"accessToken,omitempty"` Version int `json:"version"` } type VimeoConnectionResponse struct { ConnectionID string `json:"connectionId"` Name string `json:"name"` Description string `json:"description"` ClientIdentifier string `json:"clientIdentifier"` Type string `json:"type"` Enabled bool `json:"enabled"` CreatedDate string `json:"createdDate"` UpdatedDate string `json:"updatedDate"` Version int `json:"version"` } func updateVimeoConnection(apiKey string, connectionId string, updates VimeoConnectionUpdate) (*VimeoConnectionResponse, error) { url := fmt.Sprintf("https://api.pictory.ai/pictoryapis/v1/vimeo-connections/%s", connectionId) jsonData, err := json.Marshal(updates) if err != nil { return nil, err } req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData)) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var result VimeoConnectionResponse if err := json.Unmarshal(body, &result); err != nil { return nil, err } return &result, nil } // Usage func main() { connectionId := "20251222155613307xv0nodhitf9cd0f" // First, get current connection to retrieve version // (Assuming a getCurrentConnection function exists) currentVersion := 1 // Replace with actual version from GET request // Partial update - only update description partialUpdate := VimeoConnectionUpdate{ Description: "Updated description", Version: currentVersion, } result, err := updateVimeoConnection("YOUR_API_KEY", connectionId, partialUpdate) if err != nil { panic(err) } fmt.Printf("Partial Update Result: %+v\n", result) fmt.Printf("New Version: %d\n", result.Version) // Full update - update multiple fields enabled := false fullUpdate := VimeoConnectionUpdate{ Name: "Updated Connection Name", Description: "Updated description for marketing videos", Enabled: &enabled, ClientIdentifier: "new_client_id_456", ClientSecret: "new_secret_xyz789", AccessToken: "new_token_1234567890", Version: result.Version, // Use updated version } fullResult, err := updateVimeoConnection("YOUR_API_KEY", connectionId, fullUpdate) if err != nil { panic(err) } fmt.Printf("Full Update Result: %+v\n", fullResult) fmt.Printf("New Version: %d\n", fullResult.Version) } ``` *** ## Optimistic Locking This endpoint uses optimistic locking to prevent conflicts from concurrent updates: 1. **Get Current Version:** Retrieve the connection using the [Get Vimeo Connection by ID](/api-reference/vimeo-integration/get-vimeo-connection-by-id) endpoint to get the current version number 2. **Include Version:** Include the current `version` number in your update request 3. **Version Check:** The API verifies the version matches before applying changes 4. **Version Increment:** On successful update, the version number is automatically incremented 5. **Conflict Handling:** If another update occurred between your GET and PUT requests, the version will not match and the update fails **Example workflow:** ```http theme={null} # Step 1: Get current connection GET https://api.pictory.ai/pictoryapis/v1/vimeo-connections/abc123 # Response includes: "version": 5 # Step 2: Update with current version PUT https://api.pictory.ai/pictoryapis/v1/vimeo-connections/abc123 { "name": "New Name", "version": 5 } # Response includes: "version": 6 (incremented) ``` *** ## Error Handling **Cause:** The required `version` field is missing from the request body **Solution:** * Always include the current `version` number in your update request * Get the current version using the [Get Vimeo Connection by ID](/api-reference/vimeo-integration/get-vimeo-connection-by-id) endpoint * The `version` field is required for optimistic locking to prevent concurrent update conflicts **Cause:** A connection with the specified name already exists in your account **Solution:** * Choose a unique name for your connection * Keep the current name if you are only updating other fields * Use the [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) endpoint to see existing connection names **Cause:** The version number does not match the current version (someone else updated the connection) **Solution:** * Get the latest connection details using the [Get Vimeo Connection by ID](/api-reference/vimeo-integration/get-vimeo-connection-by-id) endpoint * Use the current `version` number from that response * Review the changes made by the other update before proceeding * Retry your update request with the new version number This is called "optimistic locking" and prevents concurrent updates from overwriting each other. **Cause:** Invalid or missing API key **Solution:** * Verify your API key is correct and starts with `pictai_` * Check the `Authorization` header is properly formatted: `YOUR_API_KEY` * Ensure your API key hasn't expired * Get a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** The connection ID does not exist or you do not have access to it **Solution:** * Verify the connection ID is correct and complete * Ensure the connection belongs to your account * Check if the connection has been deleted * Connection IDs are case-sensitive - confirm the exact casing * Use the [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) endpoint to list all your connections *** ## Update Strategies ### Partial Updates Only include the `version` field and the fields you want to update. All other fields remain unchanged. **Example - Update only description:** ```json theme={null} { "description": "New description", "version": 1 } ``` ### Full Updates Update multiple fields in a single request: **Example - Update multiple fields:** ```json theme={null} { "name": "New Connection Name", "description": "New description", "enabled": false, "clientIdentifier": "new_client_id", "clientSecret": "new_client_secret", "accessToken": "new_access_token", "version": 1 } ``` ### Credential Rotation When rotating Vimeo credentials (client secret or access token): 1. Obtain new credentials from the [Vimeo Developer Portal](https://developer.vimeo.com/apps) 2. Get the current version of the connection 3. Update with the new credentials and current version 4. Test the connection to ensure the new credentials work 5. Revoke the old credentials in Vimeo if needed **Disabling Connections:** Setting `enabled: false` prevents the connection from being used for Vimeo operations, but all configuration is retained. Re-enable it later by setting `enabled: true`. # Get Voiceover Tracks Source: https://docs.pictory.ai/api-reference/voiceovers/get-voiceover-tracks GET https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks Retrieve a comprehensive list of all available AI voiceover voices with their language, accent, and service provider details ## Overview Retrieve a complete list of all AI voiceover voices available for text-to-speech conversion in your video projects. The endpoint provides detailed information about each voice including accent, gender, language, service provider (AWS Polly or Google WaveNet/Neural2), sample audio URLs, and SSML support categories. You need a valid API key to use this endpoint. Get your API key from the [API Access page](https://app.pictory.ai/api-access) in your Pictory dashboard. *** ## API Endpoint ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks ``` *** ## Request Parameters ### Headers API key for authentication (starts with `pictai_`) ``` Authorization: YOUR_API_KEY ``` *** ## Response Returns an array of voice objects with the following properties: The accent or regional variant of the voice (e.g., "American accent", "British accent", "Indian accent") Voice quality category, typically "standard" The text-to-speech engine type (e.g., "neural", "WaveNet", "Neural2", "standard") The voice gender: "male" or "female" Unique numeric identifier for the voice Language code in IETF format (e.g., "en-US", "en-GB", "fr-FR", "es-ES") The display name of the voice (e.g., "Joanna", "Matthew", "Amy") URL to an MP3 sample of the voice for preview The voice provider service: "aws" (Amazon Polly) or "google" (Google Cloud Text-to-Speech) URL to documentation for supported SSML tags for this voice SSML support level category (A, B, or C) indicating which SSML features are supported The technical voice identifier used by the service provider ### Response Examples ```json 200 - Success theme={null} [ { "accent": "American accent", "category": "standard", "engine": "neural", "gender": "female", "id": 1001, "language": "en-US", "name": "Joanna", "sample": "https://pictory-static.pictorycontent.com/polly/samples/Joanna_100_sample.mp3", "service": "aws", "ssmlHelp": "https://docs.pictory.ai/docs/supported-ssml-tags#category-b", "ssmlSupportCategory": "B", "voice": "Joanna" }, { "accent": "British accent", "category": "standard", "engine": "WaveNet", "gender": "female", "id": 1034, "language": "en-GB", "name": "Fiona", "sample": "https://pictory-static.pictorycontent.com/google/samples/Fiona_en-GB-Wavenet-A_FEMALE_updated.mp3", "service": "google", "ssmlHelp": "https://docs.pictory.ai/docs/supported-ssml-tags#category-c", "ssmlSupportCategory": "C", "voice": "en-GB-Wavenet-A_FEMALE" }, { "accent": "Indian accent", "category": "standard", "engine": "WaveNet", "gender": "female", "id": 1039, "language": "en-IN", "name": "Shreya", "sample": "https://pictory-static.pictorycontent.com/google/samples/en-IN-Wavenet-A_FEMALE_updated.mp3", "service": "google", "ssmlHelp": "https://docs.pictory.ai/docs/supported-ssml-tags#category-c", "ssmlSupportCategory": "C", "voice": "en-IN-Wavenet-A_FEMALE" } ] ``` ```json 401 - Unauthorized theme={null} { "message": "Unauthorized" } ``` ```json 400 - Bad Request theme={null} { "code": "INVALID_REQUEST", "message": "Bad request" } ``` *** ## Code Examples Replace `YOUR_API_KEY` with your actual API key that starts with `pictai_` ```bash cURL theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks \ --header 'Authorization: YOUR_API_KEY' \ --header 'accept: application/json' | python -m json.tool ``` ```python Python theme={null} import requests url = "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" headers = { "Authorization": "YOUR_API_KEY", "accept": "application/json" } response = requests.get(url, headers=headers) voices = response.json() # Print voice information for voice in voices: print(f"{voice['name']} ({voice['accent']}, {voice['gender']})") print(f" Language: {voice['language']}") print(f" Engine: {voice['engine']} ({voice['service']})") print(f" ID: {voice['id']}") print(f" Sample: {voice['sample']}") print() ``` ```javascript JavaScript / Node.js theme={null} const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks', { method: 'GET', headers: { 'Authorization': 'YOUR_API_KEY', 'accept': 'application/json' } } ); const voices = await response.json(); // Print voice information voices.forEach(voice => { console.log(`${voice.name} (${voice.accent}, ${voice.gender})`); console.log(` Language: ${voice.language}`); console.log(` Engine: ${voice.engine} (${voice.service})`); console.log(` ID: ${voice.id}`); console.log(` Sample: ${voice.sample}`); console.log(''); }); ``` ```php PHP theme={null} getMessage() . "\n"; } ?> ``` ```go Go theme={null} // Replace "YOUR_API_KEY" with your actual API key package main import ( "encoding/json" "fmt" "io" "net/http" ) type VoiceoverTrack struct { ID int `json:"id"` Name string `json:"name"` Accent string `json:"accent"` Gender string `json:"gender"` Language string `json:"language"` Sample string `json:"sample"` Service string `json:"service"` Engine string `json:"engine"` Category string `json:"category"` Voice string `json:"voice"` SSMLSupportCategory string `json:"ssmlSupportCategory"` SSMLHelp string `json:"ssmlHelp"` } func getVoiceoverTracks(apiKey string) ([]VoiceoverTrack, error) { url := "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", apiKey) req.Header.Set("Accept", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed (status %d): %s", resp.StatusCode, string(body)) } var voices []VoiceoverTrack if err := json.Unmarshal(body, &voices); err != nil { return nil, err } return voices, nil } // Usage func main() { voices, err := getVoiceoverTracks("YOUR_API_KEY") if err != nil { panic(err) } fmt.Printf("Total voices: %d\n\n", len(voices)) // Print voice information for _, voice := range voices { fmt.Printf("%s (%s, %s)\n", voice.Name, voice.Accent, voice.Gender) fmt.Printf(" Language: %s\n", voice.Language) fmt.Printf(" Engine: %s (%s)\n", voice.Engine, voice.Service) fmt.Printf(" ID: %d\n", voice.ID) fmt.Printf(" Sample: %s\n\n", voice.Sample) } } ``` *** ## Usage Notes **Voice Availability**: The endpoint returns all available voices across multiple languages, accents, and service providers. Filter the results based on your project requirements. **Sample Audio**: Each voice includes a `sample` URL pointing to an MP3 preview. Use these samples to let users preview voices before selecting. **SSML Support**: Different voices support different SSML (Speech Synthesis Markup Language) features. Check the `ssmlSupportCategory` and `ssmlHelp` fields to understand what is available for each voice. **Service Providers**: * **AWS Polly** voices (`service: "aws"`) use the "neural" or "standard" engine * **Google Cloud** voices (`service: "google"`) use "WaveNet" or "Neural2" engines Generally, neural/WaveNet voices sound more natural than standard voices. *** ## Common Use Cases ### 1. List All Available Voices Retrieve and display all available voices: ```python theme={null} import requests def get_all_voices(api_key): """ Retrieve all available voiceover voices """ url = "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) if response.status_code == 200: voices = response.json() print(f"Total available voices: {len(voices)}\n") for voice in voices[:10]: # Show first 10 print(f"{voice['name']} - {voice['accent']} ({voice['language']})") print(f" Gender: {voice['gender']}, Engine: {voice['engine']}") print(f" Sample: {voice['sample']}") print() return voices else: print(f"Error: {response.status_code}") return [] # Example usage voices = get_all_voices("YOUR_API_KEY") ``` ### 2. Filter Voices by Language Get all voices for a specific language: ```javascript theme={null} async function getVoicesByLanguage(apiKey, languageCode) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks', { headers: { 'Authorization': `${apiKey}` } } ); const allVoices = await response.json(); // Filter by language code (e.g., "en-US", "en-GB", "fr-FR") const filtered = allVoices.filter(voice => voice.language === languageCode); console.log(`Found ${filtered.length} voices for ${languageCode}:`); filtered.forEach(voice => { console.log(` - ${voice.name} (${voice.accent}, ${voice.gender})`); }); return filtered; } // Example usage const usVoices = await getVoicesByLanguage('YOUR_API_KEY', 'en-US'); ``` ### 3. Group Voices by Accent Organize voices by accent type: ```python theme={null} from collections import defaultdict import requests def group_voices_by_accent(api_key): """ Group voices by their accent """ url = "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) voices = response.json() # Group by accent by_accent = defaultdict(list) for voice in voices: accent = voice.get('accent', 'Unknown') by_accent[accent].append(voice) # Print grouped results for accent, voice_list in sorted(by_accent.items()): print(f"\n{accent} ({len(voice_list)} voices):") for voice in voice_list[:5]: # Show first 5 per accent print(f" - {voice['name']} ({voice['gender']}, {voice['engine']})") return by_accent # Example usage grouped = group_voices_by_accent("YOUR_API_KEY") ``` ### 4. Find Best Voice Match Find voices matching specific criteria: ```javascript theme={null} async function findVoiceMatch(apiKey, criteria) { const response = await fetch( 'https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks', { headers: { 'Authorization': `${apiKey}` } } ); const voices = await response.json(); // Filter by multiple criteria const matches = voices.filter(voice => { return ( (!criteria.language || voice.language === criteria.language) && (!criteria.gender || voice.gender === criteria.gender) && (!criteria.accent || voice.accent.toLowerCase().includes(criteria.accent.toLowerCase())) && (!criteria.service || voice.service === criteria.service) && (!criteria.engine || voice.engine === criteria.engine) ); }); console.log(`Found ${matches.length} matching voices:`); matches.forEach(voice => { console.log(` - ${voice.name} (ID: ${voice.id})`); console.log(` ${voice.accent}, ${voice.gender}, ${voice.engine}`); }); return matches; } // Example usage - find American female neural voices const matches = await findVoiceMatch('YOUR_API_KEY', { language: 'en-US', gender: 'female', accent: 'American', engine: 'neural' }); ``` ### 5. Create Voice Selection UI Data Prepare voice data for a user interface: ```python theme={null} import requests def prepare_voice_ui_data(api_key, language_filter=None): """ Prepare voice data structured for UI dropdowns/selectors """ url = "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) voices = response.json() # Filter by language if specified if language_filter: voices = [v for v in voices if v['language'] == language_filter] # Structure for UI ui_data = { 'languages': {}, 'accents': {}, 'genders': ['male', 'female'], 'engines': set(), 'voices': [] } for voice in voices: # Collect unique languages lang = voice['language'] if lang not in ui_data['languages']: ui_data['languages'][lang] = [] ui_data['languages'][lang].append(voice['name']) # Collect unique accents accent = voice['accent'] if accent not in ui_data['accents']: ui_data['accents'][accent] = [] ui_data['accents'][accent].append(voice['name']) # Collect engines ui_data['engines'].add(voice['engine']) # Create simplified voice entry ui_data['voices'].append({ 'id': voice['id'], 'name': voice['name'], 'label': f"{voice['name']} ({voice['accent']}, {voice['gender']})", 'language': voice['language'], 'accent': voice['accent'], 'gender': voice['gender'], 'engine': voice['engine'], 'service': voice['service'], 'sample': voice['sample'] }) ui_data['engines'] = sorted(list(ui_data['engines'])) print(f"Prepared UI data for {len(ui_data['voices'])} voices") print(f"Languages: {len(ui_data['languages'])}") print(f"Accents: {len(ui_data['accents'])}") print(f"Engines: {ui_data['engines']}") return ui_data # Example usage ui_data = prepare_voice_ui_data("YOUR_API_KEY", language_filter="en-US") # Access structured data print("\nSample voice entries:") for voice in ui_data['voices'][:3]: print(f" {voice['label']} - ID: {voice['id']}") ``` ### 6. Compare Voice Providers Analyze and compare voices by service provider: ```python theme={null} import requests def compare_voice_providers(api_key): """ Compare voice offerings between AWS and Google """ url = "https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks" headers = {"Authorization": api_key} response = requests.get(url, headers=headers) voices = response.json() # Separate by provider aws_voices = [v for v in voices if v['service'] == 'aws'] google_voices = [v for v in voices if v['service'] == 'google'] print("Voice Provider Comparison:") print(f"\nAWS Polly: {len(aws_voices)} voices") print(f" Engines: {set(v['engine'] for v in aws_voices)}") print(f" Languages: {len(set(v['language'] for v in aws_voices))}") print(f" Male: {len([v for v in aws_voices if v['gender'] == 'male'])}") print(f" Female: {len([v for v in aws_voices if v['gender'] == 'female'])}") print(f"\nGoogle Cloud: {len(google_voices)} voices") print(f" Engines: {set(v['engine'] for v in google_voices)}") print(f" Languages: {len(set(v['language'] for v in google_voices))}") print(f" Male: {len([v for v in google_voices if v['gender'] == 'male'])}") print(f" Female: {len([v for v in google_voices if v['gender'] == 'female'])}") # Language overlap aws_langs = set(v['language'] for v in aws_voices) google_langs = set(v['language'] for v in google_voices) common_langs = aws_langs & google_langs print(f"\nLanguages available in both: {len(common_langs)}") print(f"AWS only: {len(aws_langs - google_langs)}") print(f"Google only: {len(google_langs - aws_langs)}") return { 'aws': aws_voices, 'google': google_voices, 'stats': { 'aws_count': len(aws_voices), 'google_count': len(google_voices), 'common_languages': list(common_langs) } } # Example usage comparison = compare_voice_providers("YOUR_API_KEY") ``` *** ## Best Practices ### Voice Selection 1. **Preview Before Use**: Always use the `sample` URLs to let users preview voices before selection 2. **Filter by Language**: Filter voices by the target language to present relevant options to users 3. **Consider Accent**: Match voice accent to your target audience (American, British, Indian, etc.) 4. **Engine Quality**: Neural and WaveNet voices generally sound more natural than standard voices 5. **SSML Support**: Check `ssmlSupportCategory` if you need advanced SSML features like custom pronunciation or emphasis ### Performance Tips * **Cache Voice List**: Cache the voice list for 24 hours as it rarely changes * **Client-Side Filtering**: Fetch all voices once and filter on the client side * **Lazy Load Samples**: Only load audio samples when users preview them * **Index by ID**: Create an ID-to-voice mapping for quick lookups ### Common Voice Categories **SSML Support Categories:** * **Category A**: Basic SSML support (standard engines) * **Category B**: Advanced SSML support (AWS neural voices) * **Category C**: Full SSML support (Google WaveNet/Neural2 voices) **Engine Types:** * **standard**: Basic quality, faster processing * **neural**: High quality, natural-sounding (AWS) * **WaveNet**: Premium quality (Google) * **Neural2**: Latest generation neural voices (Google) ### Language Coverage The API provides voices for multiple languages including: * English variants: en-US, en-GB, en-AU, en-IN, en-NZ, en-ZA * European: fr-FR, fr-CA, de-DE, de-AT, it-IT, es-ES, nl-NL, nl-BE, pt-PT, pt-BR * And many more... # Product updates Source: https://docs.pictory.ai/changelog Product updates, new features, and improvements to the Pictory API ## PowerPoint Animated Slides New scene-level field `animatePPT` enables AI-generated slide animations (zoom, pan, fade) for image-heavy PowerPoint decks. Only valid when used with `pptUrl`. * New guide: [PowerPoint to Video with Animated Slides](/guides/presentation-to-video/powerpoint-animated-slides) — covers `animatePPT`, multilingual narration, feature combinations, and best practices. * Existing [PowerPoint AI Voiceover](/guides/presentation-to-video/powerpoint-ai-voiceover) and [Speaker Notes](/guides/presentation-to-video/powerpoint-speaker-notes) guides now document both `animatePPT` and the top-level `language` field. * The field is also documented in the [Create Storyboard Preview API reference](/api-reference/videos/create-storyboard-preview). ## LLM-Friendly Documentation New resources that make the Pictory API easier to consume for Large Language Models, code generators, and agentic tools. * **OpenAPI 3.1 specification** — Available at [`docs.pictory.ai/openapi.json`](https://docs.pictory.ai/openapi.json). Covers the five most-used endpoints (Create Storyboard Preview, Render Storyboard Video, Render Project, Get Job by ID, Get Video Brands) with 34 component schemas. Use it for SDK code generation, Postman or Insomnia import, or as a structured tool schema for LLMs. * **End-to-End Recipes guide** — Ten complete, ready-to-run JSON payloads for the most common use cases, including text-to-video, multilingual narration, PowerPoint, avatar walk-throughs, template-based renders, brand kits, webhooks, previews, and job polling. * **Use Pictory with an LLM guide** — Recommended system prompt, example natural-language to API-call mappings, and Pictory MCP server pointers. * **AI Tools setup guides** — Project rules and configuration templates for [Claude Code](/ai-tools/claude-code), [Cursor](/ai-tools/cursor), and [Windsurf](/ai-tools/windsurf). Each guide teaches the tool the Pictory API conventions, including the raw-key authentication pattern and mutually-exclusive field constraints. * **`llms.txt` and `llms-full.txt`** — Mintlify auto-generates these at [`docs.pictory.ai/llms.txt`](https://docs.pictory.ai/llms.txt) and [`docs.pictory.ai/llms-full.txt`](https://docs.pictory.ai/llms-full.txt). Hand either URL to any LLM to make it Pictory-aware in one shot. ## AI Credits — Unified Currency for Generative AI Features AI Credits are now a single unified currency for Pictory's generative AI features — AI Image generation, AI Video generation, and AI Avatars. API customers receive AI Credits as part of their plan; unused credits roll over to the next billing cycle and never expire while the subscription stays active. Track consumption and remaining balance with the [Get Current Quota](/api-reference/account/get-quota) and [Get AI Credits Usage](/api-reference/account/get-aicredits-usage) endpoints. For the full breakdown of per-model rate cards, add-on packs, and Teams pooling, see [Understanding AI Credits](https://kb.pictory.ai/en/articles/12382079-understanding-ai-credits) in the Pictory Help Center. ## Account and Usage API Two new endpoints for monitoring your subscription usage and AI credit consumption. **Get Current Quota** * Retrieve real-time usage for video minutes, transcription minutes, ElevenLabs voice-over minutes, and AI credits. * Each metric shows `used` and `limit` values for the current billing term. * Use this to display usage summaries, gate features based on remaining quota, or verify capacity before launching jobs. **Get AI Credits Usage** * Retrieve a per-transaction ledger of AI credit activity with date, amount, source, and direction (credit or debit). * Filter by year and month to query specific billing periods. * Transaction sources include AI Avatar, Text-to-Image, AI Video, Add-on Purchase, and Promotional Credits. * History available for the last 12 months. See the [Get Current Quota API](/api-reference/account/get-quota) and [Get AI Credits Usage API](/api-reference/account/get-aicredits-usage) for full documentation. ## ElevenLabs Voice Auto-Discovery You can now use any voice from the ElevenLabs catalog without manual setup. Pass the ElevenLabs voice ID directly as the `speaker` value in your voiceover configuration, and Pictory will automatically discover and add the voice to your library. * **No manual voice registration** — the Add Voiceover Track endpoint is no longer needed for ElevenLabs voices. * **Pass any ElevenLabs voice ID** as the `speaker` field (e.g., `"pNInz6obpgDQGcFmaJgB"`). * **Automatic library addition** — once discovered, the voice is available by name or track ID in future requests. * **Premium voice settings** — fine-tune stability, similarity boost, style, and model selection. **Breaking change:** The `POST /v1/voiceovers/tracks` endpoint has been removed. ElevenLabs voices are now added automatically on first use. See the [ElevenLabs Voices Guide](/guides/voice-over/elevenlabs-voices) for full documentation. ## AI Visuals: Visual Continuity, Reference Images, and Creative Direction New features for AI-generated scene backgrounds that give you greater creative control over video and image generation. **Visual Continuity** * Enable `visualContinuity` to create seamless transitions between consecutive AI-generated scenes. * Works with both video clips and images. * The system uses the output of each scene as a reference for the next, producing a cohesive visual flow. * Continuity applies within the same story and across consecutive stories when enabled. **First Frame Image (Video Only)** * Use `firstFrameImageUrl` to control the starting frame of an AI-generated video clip. * The AI model generates a video that begins from your provided image and transitions into the motion described by the prompt. **Reference Image for Images** * Use `referenceImageUrl` to guide the style and composition of AI-generated images with a reference image. * Influence the color palette, lighting, and visual tone while generating new content from your prompt. **Reference Images for Videos** * Use `referenceImageUrls` to provide 1–2 reference images that guide the style of AI-generated video clips. * When using `veo3.1` or `veo3.1_fast` with reference images, the video duration is automatically set to `"8s"`. **Creative Direction Prompts** * When a story is split into multiple scenes, the `prompt` field acts as a creative direction for the entire video. * The system uses your creative direction to guide the auto-generated prompts for each individual scene. * Recommended structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. **AI Credits Tracking** * Job responses now include `aiCreditsUsed` when AI visuals are generated, reporting total credits consumed across all scenes. See the [Visual Continuity Guide](/guides/ai-generated-visuals/visual-continuity), [First Frame Image Guide](/guides/ai-generated-visuals/first-frame-image), [Reference Image Guide](/guides/ai-generated-visuals/reference-image), [Reference Images for Video Guide](/guides/ai-generated-visuals/reference-images-for-video), and the [Create Storyboard Preview API Reference](/api-reference/videos/create-storyboard-preview) for full documentation. ## AI Studio: AI Image and Video Generation Pictory now offers AI Studio, a new set of APIs for generating AI-powered images and videos directly from text prompts. AI Studio gives you access to multiple AI models, aspect ratios, visual styles, and advanced creative input modes. **Image Generation** * Generate images from text prompts with your choice of AI model and visual style. * Use a reference image to create variations, replace subjects, or apply transformations. * Choose from models including `seedream3.0`, `flux-schnell`, `nanobanana`, and `nanobanana-pro`. * Apply visual styles such as `photorealistic`, `artistic`, `cartoon`, `minimalist`, `vintage`, and `futuristic`. **Video Generation** * Generate videos from text prompts with configurable duration and aspect ratio. * Start a video from a specific image using the first frame input for precise visual control. * Extend an existing video with new AI-generated content that continues from the original. * Provide up to three reference images to guide subject appearance and scene composition. * Choose from models including `veo3.1`, `veo3.1_fast`, and `pixverse5.5`. **Browse Your Generations** * Retrieve paginated lists of all your generated images and videos. * Results are sorted by creation date with the most recent items first. **AI Credits Transparency** * Every completed job reports the number of AI credits consumed. * Image generation is charged per image, and video generation is charged per second of output. * See the rate card in each model's documentation for exact pricing. See the [AI Studio API Reference](/api-reference/ai-studio/generate-image) and the [AI Studio Guides](/guides/ai-studio/text-to-image) for full documentation. ## Dynamic Captions with Word-Level Timing Dynamic captions now render subtitles word by word, synchronized precisely with the voiceover audio. This produces a more engaging viewing experience where each word appears on screen as it is spoken. * Set `maxSubtitleLines` to a value from `1` to `4` to control how many lines of subtitles are displayed at a time. * Word-level timing is applied by default when dynamic captions are active. See the [Dynamic Captions Guide](/guides/captions/dynamic-captions) for usage details. # Background Music Source: https://docs.pictory.ai/guides/advanced-features/background-music Add background music to videos with custom tracks or AI-selected music for enhanced engagement This guide shows you how to add background music to your videos. Choose between providing your own custom music files or letting AI select appropriate tracks automatically. Perfect for creating engaging videos with professional audio layers. ## What You'll Learn Add your own music tracks from URLs Let AI choose appropriate background music Adjust music volume for perfect balance Use specific portions of music tracks ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Music file accessible via public URL (for custom music) * Basic understanding of video creation with Pictory API ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Background Music Works When you add background music to a video: 1. **Music Selection** - You provide a custom music URL or enable AI auto-selection 2. **Audio Processing** - The music file is accessed and processed 3. **Volume Adjustment** - Music volume is set according to your specifications 4. **Clip Extraction** - Specific portions of the music are extracted (if clips defined) 5. **Audio Mixing** - Music is mixed with voice-over and video audio 6. **Duration Matching** - Music loops or fades to match video length 7. **Video Rendering** - Final video is rendered with all audio layers combined Background music automatically loops if the music track is shorter than your video duration. The music fades out gracefully at the end of the video for a professional finish. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Your custom music file URL (must be publicly accessible) const MUSIC_URL = "https://example.com/your-music-file.mp3"; const STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createVideoWithBackgroundMusic() { try { console.log("Creating video with background music..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_background_music", // Background music configuration backgroundMusic: { enabled: true, // Enable background music musicUrl: MUSIC_URL, // Your custom music file URL volume: 0.3, // 30% volume (0-1) // Optional: Use specific portions of the music clips: [ { start: 10, // Start at 10 seconds end: 70, // End at 70 seconds (60s clip) }, ], }, // Scene configuration scenes: [ { story: STORY_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with background music is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithBackgroundMusic(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Your custom music file URL (must be publicly accessible) MUSIC_URL = "https://example.com/your-music-file.mp3" STORY_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_video_with_background_music(): try: print("Creating video with background music...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_background_music', # Background music configuration 'backgroundMusic': { 'enabled': True, # Enable background music 'musicUrl': MUSIC_URL, # Your custom music file URL 'volume': 0.3, # 30% volume (0-1) # Optional: Use specific portions of the music 'clips': [ { 'start': 10, # Start at 10 seconds 'end': 70 # End at 70 seconds (60s clip) } ] }, # Scene configuration 'scenes': [ { 'story': STORY_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with background music is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_background_music() ``` ## Background Music Configuration Parameters ### Music Settings | Parameter | Type | Required | Description | | --------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------- | | `backgroundMusic.enabled` | boolean | Yes | Set to `true` to enable background music in the video | | `backgroundMusic.musicUrl` | string | No | Public URL to your custom music file. Cannot be used with `autoMusic`. | | `backgroundMusic.autoMusic` | boolean | No | Set to `true` to let AI select appropriate background music. Cannot be used with `musicUrl`. | | `backgroundMusic.volume` | number | No | Music volume level from 0 (muted) to 1 (full volume). Default: 0.5 | | `backgroundMusic.clips` | array | No | Array of music clip objects defining which portions of the music to use. Maximum 10 clips. | ### Music Clips Configuration | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------------------------------------------------- | | `clips[].start` | number | Yes | Start time in seconds where the music clip begins | | `clips[].end` | number | Yes | End time in seconds where the music clip ends. Must be greater than `start`. | **Mutual Exclusivity:** You cannot use both `musicUrl` and `autoMusic` in the same request. Choose one: * Use `musicUrl` for custom music tracks * Use `autoMusic: true` for AI-selected music ## Supported Audio Formats | Format | Extension | Description | Best Used For | | ------ | --------- | -------------------------------------- | -------------------------------------- | | MP3 | `.mp3` | Most common audio format (recommended) | General use, best compatibility | | WAV | `.wav` | Uncompressed audio, high quality | High-quality music, professional audio | | AAC | `.aac` | Advanced audio codec | Modern devices, streaming | | M4A | `.m4a` | Apple audio format | Music purchased from iTunes | | FLAC | `.flac` | Lossless compression | Audiophile quality, large files | | OGG | `.ogg` | Open-source format | Web streaming, game audio | **Best Format:** Use MP3 with 128-320 kbps bitrate for the best balance of quality and processing speed. Ensure music files are accessible via public, direct download URLs. ## Volume Level Reference Choose the right volume based on your video's audio composition: | Volume Value | Percentage | Effect | Best Used For | | ------------ | ---------- | ---------------- | -------------------------------------------------- | | 0 | 0% | Muted | No background music | | 0.1-0.2 | 10-20% | Very subtle | Videos with heavy narration, podcasts | | 0.3-0.4 | 30-40% | Soft background | Videos with voice-over, professional presentations | | 0.5 | 50% | Balanced | General use, balanced audio mix | | 0.6-0.7 | 60-70% | Noticeable music | Videos without voice-over, music videos | | 0.8-0.9 | 80-90% | Prominent music | Emphasis on music, minimal narration | | 1.0 | 100% | Full volume | Pure background music, no other audio | **Voice-Over Recommendations:** * **With AI voice-over**: Use 0.2-0.4 to ensure narration is clearly heard * **Without voice-over**: Use 0.5-0.8 for more prominent background music * **With original video audio**: Use 0.2-0.3 to avoid overwhelming existing audio ## Using AI-Selected Music Let the AI automatically choose appropriate background music for your video: ```javascript Node.js theme={null} { backgroundMusic: { enabled: true, autoMusic: true, // AI selects appropriate music volume: 0.4 // Adjust volume as needed } } ``` ```python Python theme={null} { 'backgroundMusic': { 'enabled': True, 'autoMusic': True, # AI selects appropriate music 'volume': 0.4 # Adjust volume as needed } } ``` **How AI Selection Works:** * AI analyzes your video content and theme * Selects music that matches the video's mood and pacing * Chooses from a library of royalty-free music tracks * No need to provide your own music URL ## Using Music Clips Extract and use specific portions of a music track: ### Single Clip Example ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://example.com/music.mp3", volume: 0.5, clips: [ { start: 30, end: 90 } // Use seconds 30-90 (1 minute clip) ] } } ``` **Result:** Uses only seconds 30-90 from the music file, looping if needed. ### Multiple Clips Example ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://example.com/music.mp3", volume: 0.4, clips: [ { start: 0, end: 30 }, // Intro section (30 seconds) { start: 90, end: 120 }, // Chorus section (30 seconds) { start: 180, end: 210 } // Outro section (30 seconds) ] } } ``` **Result:** Uses three different portions of the music, played sequentially. ### Full Track (No Clips) ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://example.com/music.mp3", volume: 0.5 // No clips specified - uses entire track } } ``` **Result:** Uses the entire music track from start to finish. **Clip Strategy:** Use clips to extract the best portions of a song (e.g., just the upbeat chorus, skip long intros). Maximum 10 clips per video. ## Common Use Cases ### Educational Videos with Narration ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, // AI-selected background music volume: 0.25 // Quiet to not overpower voice-over }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 100 }] } } ``` **Result:** Subtle background music that enhances the video without interfering with educational narration. ### Marketing Videos with Energetic Music ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://storage.example.com/upbeat-corporate.mp3", volume: 0.6, // Prominent but not overwhelming clips: [ { start: 15, end: 75 } // Use the energetic chorus section ] } } ``` **Result:** Engaging, energetic music that drives the marketing message forward. ### Social Media Content (No Voice-Over) ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://storage.example.com/trending-track.mp3", volume: 0.75 // Higher volume, no voice-over to compete with } } ``` **Result:** Music-driven social media video with subtitles for context. ### Professional Presentations ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://storage.example.com/corporate-ambient.mp3", volume: 0.3, // Professional, subtle background clips: [ { start: 0, end: 120 } // Use calm intro section ] }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", speed: 95 }] } } ``` **Result:** Professional presentation with subtle, non-distracting background music and clear narration. ## Best Practices Ensure music complements rather than competes with narration: * **With Voice-Over**: Use volume 0.2-0.4 for background music * **Clear Narration**: Keep music soft enough that every word is heard * **Voice Emphasis**: Music should enhance, not distract from the message * **Test Levels**: Preview videos to ensure proper audio balance * **Consistent Volume**: Maintain consistent music volume throughout Match music style to your video's purpose and audience: * **Educational Content**: Use calm, neutral background music * **Marketing Videos**: Choose upbeat, energetic tracks * **Professional Presentations**: Use subtle, corporate-style music * **Social Media**: Opt for trending or popular music styles * **Emotional Content**: Match music mood to video emotion (inspiring, somber, etc.) * **Brand Alignment**: Ensure music fits your brand identity Avoid copyright issues by using properly licensed music: * **Royalty-Free Libraries**: Use sites like AudioJungle, Epidemic Sound, Artlist * **Creative Commons**: Verify licenses allow commercial use * **Stock Music**: Invest in quality stock music libraries * **Original Music**: Commission custom tracks for unique branding * **AI Music**: Use AI-generated music that is copyright-safe * **Attribution**: Follow license requirements for attribution when needed Ensure your music files can be accessed by the API: * **Public URLs**: Upload to cloud storage (AWS S3, Google Drive, Dropbox) * **Direct Links**: Use direct download links, not streaming or preview URLs * **Stable URLs**: Ensure links will not expire during processing * **Test Access**: Verify URLs work in incognito browser * **File Size**: Keep files under 50MB for faster processing * **Format**: Use MP3 format for best compatibility Use music clips to enhance your video effectively: * **Skip Intros**: Start clips after long musical intros * **Use Choruses**: Extract upbeat, engaging portions of songs * **Avoid Vocals**: Consider instrumental tracks or sections for clarity * **Loop Planning**: Choose clips that loop naturally if needed * **Length Matching**: Use clips that align with your video duration * **Multiple Clips**: Vary music across different video sections (max 10) ## Troubleshooting **Problem:** The API cannot download or process your music file. **Solution:** * Verify the URL is publicly accessible (test in incognito browser) * Ensure it is a direct download link, not a streaming or preview link * For Google Drive: Right-click → Share → "Anyone with the link" → Copy link * For Dropbox: Share → Create link → change "dl=0" to "dl=1" at end of URL * Check file hasn't been deleted or moved * Verify file format is supported (MP3, WAV, AAC, M4A) * Ensure file size is reasonable (under 50MB recommended) **Problem:** Background music volume does not balance well with other audio. **Solution:** * Adjust the `volume` parameter (range: 0-1) * With voice-over: Try 0.2-0.4 for subtle background * Without voice-over: Try 0.5-0.8 for more prominent music * Test different values to find the right balance * Consider the original music file's volume level * Use audio editing software to normalize music before uploading **Problem:** Request fails because both `musicUrl` and `autoMusic` are specified. **Solution:** * Choose one method: custom music OR AI-selected music * Remove `musicUrl` if using `autoMusic: true` * Remove `autoMusic` if using `musicUrl` * Example with AI music: `{ enabled: true, autoMusic: true, volume: 0.4 }` * Example with custom music: `{ enabled: true, musicUrl: "...", volume: 0.4 }` **Problem:** Music ends abruptly or does not loop smoothly. **Solution:** * The API automatically loops music shorter than the video * Choose music that loops naturally (same start and end) * Use music editing software to create seamless loops * Consider using full-length tracks that match video duration * Use multiple clips to control exactly which portions play * Music automatically fades out at video end for smooth finish **Problem:** Music clips do not extract the expected portions of the track. **Solution:** * Verify `start` time is less than `end` time * Check that times are in seconds, not minutes * Ensure clip times do not exceed the music file duration * Test clip times using an audio player first * Maximum 10 clips per video * Clips play in the order they are defined in the array **Problem:** Created video but background music is missing. **Solution:** * Verify `enabled: true` in `backgroundMusic` configuration * Check `volume` is not set to 0 (muted) * Ensure either `musicUrl` or `autoMusic: true` is specified * Verify job completed successfully without errors * Check API response for any warnings about music processing * Test with AI music first to isolate custom URL issues * Verify music file URL is accessible and in supported format ## Music Looping and Fading ### Automatic Looping When your music track is shorter than the video: * Music automatically loops from the beginning * Seamless looping for music designed to loop * Use loop-friendly tracks for best results ### Automatic Fade-Out At the end of the video: * Music fades out gracefully over the last 2-3 seconds * Prevents abrupt audio cutoff * Creates professional-sounding finish ### Using Clips to Control Looping ```javascript theme={null} { clips: [ { start: 30, end: 90 } // Loop only the 30-90 second portion ] } ``` Only the specified clip portion loops, not the entire track. ## Next Steps Enhance your videos with these complementary features: Add narration to videos with background music Apply consistent branding with background music Add intro and outro sequences with music Style subtitles to complement your music videos ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including backgroundMusic * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Background Video Settings Source: https://docs.pictory.ai/guides/advanced-features/background-video-settings Control background video behavior with mute and loop settings for precise audio and playback control This guide shows you how to configure background video settings to control audio and looping behavior. Mute background video audio to prevent conflicts with voice-over, and control whether videos loop or play once for professional scene composition. ## What You'll Learn Control background video audio on/off Configure video looping behavior Prevent audio conflicts in scenes Fine-tune video playback behavior ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Video files for backgrounds (or using stock library) * Understanding of when to use mute/loop settings ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Background Video Settings Work When you configure background video settings: 1. **Settings Definition** - You specify mute and/or loop settings for scene 2. **Background Type Detection** - System identifies background type (video/image/color) 3. **Settings Application** - Mute and loop are applied to video backgrounds 4. **Audio Processing** - Video audio is muted if `mute: true` 5. **Loop Processing** - Video loops or plays once based on `loop` setting 6. **Duration Matching** - Video duration is handled according to loop setting 7. **Scene Rendering** - Final scene rendered with configured video behavior Background video settings only apply to video backgrounds. They are ignored for image backgrounds and color backgrounds, which have no audio or inherent duration. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media."; async function createVideoWithBackgroundSettings() { try { console.log("Creating video with background video settings..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_background_settings", scenes: [ { story: STORY_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, // Background video settings background: { settings: { mute: true, // Mute background video audio loop: false // Don't loop video, play once } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with background settings is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithBackgroundSettings(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT = ( "AI is poised to significantly impact educators and course creators on social media." ) def create_video_with_background_settings(): try: print("Creating video with background video settings...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_background_settings', 'scenes': [ { 'story': STORY_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, # Background video settings 'background': { 'settings': { 'mute': True, # Mute background video audio 'loop': False # Don't loop video, play once } } } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with background settings is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_background_settings() ``` ## Understanding the Parameters ### Background Video Settings | Parameter | Type | Default | Description | | -------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `background.settings.mute` | boolean | `true` | When `true`, mutes background video audio. When `false`, includes video's original audio. | | `background.settings.loop` | boolean | `true` | When `true`, loops video if shorter than scene duration. When `false`, video plays once and freezes on last frame. | ### Settings Application by Background Type | Background Type | Mute Applies? | Loop Applies? | Notes | | --------------- | ------------- | ------------- | -------------------------------------------------- | | Video | ✓ Yes | ✓ Yes | Both settings are applied to video playback | | Image | ✗ No | ✗ No | Images have no audio or duration, settings ignored | | Color | ✗ No | ✗ No | Color backgrounds have no audio or duration | **Important:** If `loop` is set to `false` and the background video is shorter than the scene duration, the video will freeze on its last frame for the remaining scene duration. ## Mute Setting Explained ### Mute: true (Default - Recommended) ```javascript theme={null} { background: { settings: { mute: true // Background video audio is muted } } } ``` **When to use:** * You have voice-over narration * Using background music separately * Want only visual elements from video * Prevent audio conflicts or distraction * Most common use case for background videos **Result:** Video plays visually but without its original audio. ### Mute: false ```javascript theme={null} { background: { settings: { mute: false // Background video audio plays } } } ``` **When to use:** * Background video has desired ambient sound * No voice-over or competing audio * Video audio adds value to the scene * Creating atmospheric soundscapes **Result:** Video plays with its original audio track. **Best Practice:** Always set `mute: true` when using voice-over or background music to prevent audio conflicts. Set `mute: false` only when the background video's audio is intentionally part of your audio design. ## Loop Setting Explained ### Loop: true (Default) ```javascript theme={null} { background: { settings: { loop: true // Video repeats if shorter than scene } } } ``` **When to use:** * Background video is shorter than scene duration * Want continuous motion throughout scene * Using short clips as repeating backgrounds * Creating seamless visual atmosphere **Result:** Video loops continuously until scene ends. **Example:** 5-second video in 15-second scene plays 3 times. ### Loop: false ```javascript theme={null} { background: { settings: { loop: false // Video plays once, then freezes } } } ``` **When to use:** * Intro/outro videos that should play once * Video has specific timeline or narrative * Video length matches or exceeds scene duration * Don't want repetitive motion **Result:** Video plays once, then displays last frame. **Example:** 5-second video in 15-second scene plays once, freezes last frame for 10 seconds. ## Configuration Combinations ### Combination 1: Muted Looping (Most Common) ```javascript theme={null} { background: { settings: { mute: true, // No background video audio loop: true // Repeat video throughout scene } } } ``` **Use Case:** Standard scenes with voice-over and repeating visual background. ### Combination 2: Muted Non-Looping ```javascript theme={null} { background: { settings: { mute: true, // No background video audio loop: false // Play once and freeze } } } ``` **Use Case:** Intro/outro videos with voice-over, video plays once. ### Combination 3: Audio with Looping ```javascript theme={null} { background: { settings: { mute: false, // Include video audio loop: true // Repeat video and audio } } } ``` **Use Case:** Atmospheric background with ambient sounds, no voice-over. ### Combination 4: Audio Without Looping ```javascript theme={null} { background: { settings: { mute: false, // Include video audio loop: false // Play once, then freeze } } } ``` **Use Case:** Video with narrative or specific audio timeline. ## Common Use Cases ### Educational Video with Voice-Over ```javascript theme={null} { scenes: [ { story: "Today we'll learn about marine ecosystems...", background: { type: "video", visualUrl: "https://example.com/ocean-background.mp4", settings: { mute: true, // Mute so voice-over is clear loop: true // Loop ocean footage throughout } } } ], voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma" }] } } ``` **Result:** Clear voice-over with looping ocean visuals, no audio conflicts. ### Branded Intro Sequence ```javascript theme={null} { scenes: [ { background: { type: "video", visualUrl: "https://example.com/brand-intro.mp4", settings: { mute: true, // Mute intro video loop: false // Play intro once, don't repeat } }, minimumDuration: 5 } ] } ``` **Result:** Brand intro plays once without looping, muted for clean presentation. ### Ambient Scene with Natural Sounds ```javascript theme={null} { scenes: [ { story: "Experience the tranquility of nature...", background: { type: "video", visualUrl: "https://example.com/forest-with-birds.mp4", settings: { mute: false, // Include natural forest sounds loop: true // Loop ambient video and sounds } } } ] // No voice-over - letting ambient audio create atmosphere } ``` **Result:** Video with forest sounds playing, creating immersive atmosphere. ### Product Demo Without Audio Distraction ```javascript theme={null} { scenes: [ { story: "Our product features cutting-edge technology...", background: { type: "video", visualUrl: "https://example.com/product-demo.mp4", settings: { mute: true, // Mute product demo audio loop: false // Show demo once completely } } } ], backgroundMusic: { enabled: true, musicUrl: "https://example.com/background-music.mp3", volume: 0.3 } } ``` **Result:** Product demo visual with background music instead of original demo audio. ## Best Practices Prevent audio conflicts in narrated videos: * **Default to Mute**: Set `mute: true` for all narrated content * **Audio Clarity**: Ensures voice-over is clearly heard * **No Distraction**: Background video audio can distract from message * **Professional Sound**: Clean audio mix sounds more professional * **Predictable Output**: Eliminates unexpected background audio * **Test Exception**: Only use `mute: false` intentionally for specific effect Ensure continuous motion in scenes: * **Short Clips**: Loop 3-10 second clips for longer scenes * **Seamless Loops**: Choose videos designed to loop seamlessly * **Visual Continuity**: Maintains motion throughout scene * **Stock Library**: Many stock videos are designed for looping * **Test Loops**: Preview to ensure loop points do not jar * **Duration Matching**: If video length ≥ scene, loop is automatic Play specific sequences once: * **Intro/Outro**: Set `loop: false` for branded intro/outro sequences * **Specific Timeline**: Use for videos with beginning/middle/end * **One-Time Events**: Actions that should not repeat (explosions, reveals) * **Timed Sequences**: When video timing is crucial to message * **Duration Match**: Ensure video length matches or exceeds scene duration * **Freeze Planning**: Consider if final frame makes sense frozen Manage audio layers properly: * **Mute Video**: Always `mute: true` when using background music * **One Audio Source**: Do not mix background video audio + music * **Layering**: Voice-over + music works; voice-over + video audio + music does not * **Volume Balance**: If using video audio, omit background music * **Audio Design**: Plan overall audio strategy before settings * **Professional Mix**: Fewer audio layers = cleaner sound Optimize settings per scene purpose: * **Intro Scenes**: Typically `mute: true, loop: false` * **Content Scenes**: Typically `mute: true, loop: true` * **Outro Scenes**: Typically `mute: true, loop: false` * **Ambient Scenes**: May use `mute: false` for atmosphere * **Preview First**: Test settings with actual content before finalizing * **Document Patterns**: Record which settings work for different scene types ## Troubleshooting **Problem:** Background video settings do not seem to apply. **Solution:** * Verify you are using a video background (not image or color) * Check settings are in `background.settings`, not elsewhere * Ensure JSON structure is correct with proper nesting * Settings only apply to video backgrounds, ignored for images * Test with known video URL to isolate issue * Review API response for any validation errors **Problem:** Background video audio plays despite `mute: true`. **Solution:** * Verify `mute: true` is set in `background.settings` * Check audio is not from voice-over or background music instead * Ensure you are testing the correct video output * Try explicitly setting `mute: true` (do not rely on default) * Test with different video to rule out video-specific issue * Check if audio is from a different layer in your composition **Problem:** Video keeps looping when you want it to play once. **Solution:** * Set `loop: false` explicitly in `background.settings` * Default behavior is `loop: true`, must override * Verify setting is within the scene's background configuration * Check that video length is actually shorter than scene duration * If video ≥ scene duration, looping will not be noticeable anyway * Preview output to confirm loop behavior **Problem:** Short video does not loop, plays once and stops/freezes. **Solution:** * Check if `loop: false` is accidentally set * Default is `loop: true`, so check for overrides * Verify video length is actually shorter than scene * If video matches scene length, no loop needed * Check `minimumDuration` vs actual video duration * Test with explicitly set `loop: true` **Problem:** Video plays once and freezes, but want different behavior. **Solution:** * This happens when `loop: false` and video \< scene duration * Set `loop: true` to repeat video instead of freezing * Or adjust scene duration to match video length * Or use longer video that fills entire scene * This is expected behavior with `loop: false` * Plan scenes knowing non-looping videos will freeze **Problem:** Want to control music vs effects in background video. **Solution:** * `mute` setting is all-or-nothing for video audio * Cannot selectively mute parts of video's audio track * Options: * Use video editing software to remove audio before upload * Choose background videos without audio * Use `mute: true` and add background music separately * Cannot isolate or control individual audio elements * This is a limitation of background video audio control ## Settings with Custom Video URLs When using custom video URLs with background settings: ```javascript theme={null} { background: { type: "video", visualUrl: "https://example.com/your-video.mp4", settings: { mute: true, loop: false } } } ``` **Considerations:** * Ensure video URL is publicly accessible * Video format should be MP4 for best compatibility * Test video plays correctly before applying settings * Settings apply regardless of video source (custom or stock) ## Default Behavior When no settings are specified: ```javascript theme={null} { background: { type: "video", visualUrl: "https://example.com/video.mp4" // No settings specified } } ``` **Default Values:** * `mute`: `true` (background video audio is muted by default) * `loop`: `true` (video loops by default if shorter than scene) This default behavior works well for most use cases with voice-over. ## Next Steps Enhance your background video control with these complementary features: Add video-based intro and outro sequences Add audio layers to your videos Control which videos are selected Add transitions between background videos ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including background settings * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Intro and Outro Scenes Source: https://docs.pictory.ai/guides/advanced-features/intro-outro Add branded intro images and outro videos to create professional, polished videos with consistent branding This guide shows you how to add professional intro and outro scenes to your videos. Use static images for branded intros and video clips for dynamic outros, perfect for creating consistent, professional-looking content across all your videos. ## What You'll Learn Add branded intro scenes with static images Add outro scenes with video backgrounds Set precise durations for intro/outro scenes Combine intro, content, and outro scenes ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Intro image and/or outro video accessible via public URLs * Basic understanding of scene configuration in Pictory API ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Intro and Outro Scenes Work When you add intro and outro scenes to your video: 1. **Scene Definition** - You define scenes with background visuals (images or videos) 2. **Duration Setting** - Specify minimum duration for background-only scenes 3. **Scene Ordering** - Scenes are arranged in order: intro → content → outro 4. **Visual Processing** - Background images and videos are processed and prepared 5. **Audio Integration** - Background music and audio tracks are synchronized 6. **Scene Assembly** - All scenes are combined into a single video 7. **Video Rendering** - Final video is rendered with intro, content, and outro Background-only scenes (intro/outro without text) require a `minimumDuration` to specify how long they should display. Content scenes with text calculate duration automatically based on voice-over and text length. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // URLs to your intro image and outro video (must be publicly accessible) const INTRO_IMAGE_URL = "https://example.com/your-intro-image.png"; const OUTRO_VIDEO_URL = "https://example.com/your-outro-video.mp4"; const STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createVideoWithIntroOutro() { try { console.log("Creating video with intro and outro scenes..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_intro_outro", scenes: [ // INTRO SCENE: Branded intro with static image { background: { type: "image", // Use static image visualUrl: INTRO_IMAGE_URL, // Your intro image URL }, minimumDuration: 5, // Display for 5 seconds }, // MAIN CONTENT SCENE: Generated from text { story: STORY_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, // OUTRO SCENE: Outro with video background { background: { type: "video", // Use video background visualUrl: OUTRO_VIDEO_URL, // Your outro video URL }, minimumDuration: 5, // Display for 5 seconds }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with intro and outro is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithIntroOutro(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # URLs to your intro image and outro video (must be publicly accessible) INTRO_IMAGE_URL = "https://example.com/your-intro-image.png" OUTRO_VIDEO_URL = "https://example.com/your-outro-video.mp4" STORY_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_video_with_intro_outro(): try: print("Creating video with intro and outro scenes...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_intro_outro', 'scenes': [ # INTRO SCENE: Branded intro with static image { 'background': { 'type': 'image', # Use static image 'visualUrl': INTRO_IMAGE_URL # Your intro image URL }, 'minimumDuration': 5 # Display for 5 seconds }, # MAIN CONTENT SCENE: Generated from text { 'story': STORY_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True }, # OUTRO SCENE: Outro with video background { 'background': { 'type': 'video', # Use video background 'visualUrl': OUTRO_VIDEO_URL # Your outro video URL }, 'minimumDuration': 5 # Display for 5 seconds } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with intro and outro is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_intro_outro() ``` ## Scene Configuration Parameters ### Background Configuration | Parameter | Type | Required | Description | | ---------------------- | ------ | -------- | ---------------------------------------------------------- | | `background.type` | string | Yes | Type of background visual. Options: `"image"` or `"video"` | | `background.visualUrl` | string | Yes | Public URL to the background image or video file | ### Scene Duration | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------- | | `minimumDuration` | number | Yes\* | Duration in seconds for background-only scenes. \*Required for scenes without `story` content. | **minimumDuration Required:** Scenes with only background visuals (no `story` text) must include `minimumDuration` to specify how long the scene should display. Without it, the API will not know how long to show the intro/outro. ## Supported File Formats ### Intro Images | Format | Extension | Resolution Recommendation | Notes | | -------- | --------------- | ------------------------- | ----------------------------------- | | PNG | `.png` | 1920x1080 or higher | Best for transparency, logos | | JPEG/JPG | `.jpg`, `.jpeg` | 1920x1080 or higher | Best for photographs, smaller files | | WebP | `.webp` | 1920x1080 or higher | Modern format, good compression | ### Outro Videos | Format | Extension | Recommendation | Notes | | ------ | --------- | ------------------------- | ------------------------------ | | MP4 | `.mp4` | H.264 codec (recommended) | Best compatibility | | MOV | `.mov` | H.264 codec | Apple format, widely supported | | WebM | `.webm` | VP9 codec | Modern web format | | AVI | `.avi` | Various codecs | Larger file sizes | **Best Practices:** * **Intro images**: Use PNG with 1920x1080 resolution for crisp, professional branding * **Outro videos**: Use MP4 with H.264 codec at 1080p for best quality and compatibility * **File size**: Keep images under 5MB and videos under 50MB for faster processing ## Recommended Durations Choose the right duration based on your content type and platform: | Scene Type | Duration Range | Best Used For | Platform | | -------------- | -------------- | ----------------------------------------- | ------------------------- | | Quick Intro | 2-3 seconds | Fast-paced social media, short videos | TikTok, Instagram Reels | | Standard Intro | 4-5 seconds | Most general content, professional videos | YouTube, LinkedIn | | Detailed Intro | 6-8 seconds | Tutorials, educational content | YouTube, Course platforms | | Short Outro | 3-5 seconds | Quick call-to-action, social media | Instagram, Twitter/X | | Standard Outro | 5-8 seconds | Subscribe prompts, contact information | YouTube, Facebook | | Extended Outro | 10-15 seconds | Detailed CTAs, credits, multiple links | YouTube, Webinars | **Platform-Specific Recommendations:** * **TikTok/Instagram Reels**: Keep intro under 3 seconds (viewers scroll quickly) * **YouTube**: 4-6 second intro is ideal (establishes brand without losing viewers) * **Professional/Corporate**: 5-8 seconds works well for polished presentation * **Educational**: Can use longer intros (6-8 seconds) for course branding ## Scene Types Explained ### Background-Only Scene (Intro/Outro) ```javascript theme={null} { background: { type: "image", // or "video" visualUrl: "https://..." }, minimumDuration: 5 // Required } ``` **Characteristics:** * No `story` text content * Static visual display * Fixed duration via `minimumDuration` * Perfect for branded intros and outros ### Content Scene with Text ```javascript theme={null} { story: "Your content text here...", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ``` **Characteristics:** * Has `story` text content * Duration calculated automatically from text length * AI selects background visuals automatically * `minimumDuration` not needed (optional) ### Content Scene with Custom Background ```javascript theme={null} { story: "Your content text here...", background: { type: "video", visualUrl: "https://..." } // minimumDuration optional - calculated from text } ``` **Characteristics:** * Has both text and custom background * Duration calculated from text length * Custom visual instead of AI-selected * Combines both approaches ## Common Use Cases ### YouTube Videos with Branding ```javascript theme={null} { scenes: [ // Intro: Channel branding { background: { type: "image", visualUrl: "https://storage.example.com/youtube-intro.png" }, minimumDuration: 4 // Standard YouTube intro }, // Main content { story: "Your video content...", createSceneOnNewLine: true }, // Outro: Subscribe prompt { background: { type: "video", visualUrl: "https://storage.example.com/subscribe-outro.mp4" }, minimumDuration: 8 // Time for subscribe animation } ] } ``` **Result:** Professional YouTube video with channel intro and subscribe outro. ### Social Media Quick Posts ```javascript theme={null} { scenes: [ // Quick brand flash { background: { type: "image", visualUrl: "https://storage.example.com/brand-logo.png" }, minimumDuration: 2 // Fast intro for social }, // Content { story: "Quick tip or announcement...", createSceneOnNewLine: true }, // No outro - keep it short for social media ] } ``` **Result:** Quick social media video with brief brand intro, no outro for maximum engagement retention. ### Educational Course Videos ```javascript theme={null} { scenes: [ // Course branding intro { background: { type: "video", visualUrl: "https://storage.example.com/course-intro.mp4" }, minimumDuration: 6 // Establish course branding }, // Lesson content { story: "Today's lesson content...", createSceneOnEndOfSentence: true }, // Outro with next lesson prompt { background: { type: "image", visualUrl: "https://storage.example.com/next-lesson.png" }, minimumDuration: 10 // Time to read next steps } ] } ``` **Result:** Professional educational video with course branding and next-lesson call-to-action. ### Marketing Videos with CTA ```javascript theme={null} { scenes: [ // Product/brand intro { background: { type: "image", visualUrl: "https://storage.example.com/product-intro.png" }, minimumDuration: 5 }, // Marketing message { story: "Discover our amazing new product...", createSceneOnNewLine: true }, // Strong CTA outro { background: { type: "video", visualUrl: "https://storage.example.com/cta-outro.mp4" }, minimumDuration: 7 // Time for CTA to register } ] } ``` **Result:** Marketing video with product intro and compelling call-to-action outro. ## Best Practices Viewers have short attention spans - make intros count: * **3-5 Seconds Ideal**: Most viewers will tolerate a 3-5 second intro * **Front-Load Value**: Show what the video is about quickly * **Branded but Brief**: Establish brand without being tedious * **Platform Matters**: Social media needs shorter intros (2-3s) than YouTube (4-6s) * **Skip Option**: Consider if viewers can skip long intros on your platform * **Test Retention**: Monitor if long intros cause viewer drop-off Use outros strategically to drive action: * **Clear Call-to-Action**: Tell viewers exactly what to do next * **Single Focus**: One primary CTA (subscribe, visit website, next video) * **Sufficient Duration**: 5-8 seconds for viewers to read and act * **Visual Hierarchy**: Make the CTA visually prominent * **End Screens**: For YouTube, align with end screen timing (last 20 seconds) * **Contact Info**: Include relevant links, social handles, or contact details Balance quality with processing speed: * **Resolution**: Use 1920x1080 (Full HD) for professional quality * **Image Size**: Keep images under 5MB for faster upload/processing * **Video Size**: Keep outro videos under 50MB when possible * **Compression**: Use appropriate compression without sacrificing quality * **Format**: MP4 for videos, PNG/JPEG for images for best compatibility * **Test First**: Verify files render correctly before using in production Create a cohesive brand experience: * **Reuse Assets**: Use the same intro/outro across video series * **Brand Colors**: Match intro/outro to brand color palette * **Logo Placement**: Consistent logo positioning across videos * **Typography**: Use brand fonts in intro/outro designs * **Style Guide**: Maintain visual consistency across all branded elements * **Template Library**: Build a library of reusable intro/outro templates Make sure your files can be accessed by the API: * **Public URLs**: Upload to cloud storage (AWS S3, Google Drive, Dropbox) * **Direct Links**: Use direct download URLs, not streaming or preview links * **Stable URLs**: Ensure links will not expire during processing * **Test Access**: Verify URLs work in incognito browser * **Permissions**: Check file sharing permissions are set to public * **HTTPS**: Use HTTPS URLs for security and reliability ## Troubleshooting **Problem:** API returns error about missing `minimumDuration` for intro/outro scene. **Solution:** * Background-only scenes (no `story` text) require `minimumDuration` * Add `minimumDuration` in seconds to your intro/outro scenes * Example: `{ background: {...}, minimumDuration: 5 }` * Content scenes with `story` text do not need `minimumDuration` * Verify you have not accidentally omitted this required field **Problem:** Final video does not include intro or outro scene. **Solution:** * Verify the `visualUrl` is publicly accessible (test in incognito browser) * Check file format is supported (PNG, JPEG for images; MP4, MOV for videos) * Ensure URL is a direct link, not a preview or sharing page * For Google Drive: Use "Anyone with link" sharing and get direct download URL * For Dropbox: Change "dl=0" to "dl=1" at end of share URL * Verify file hasn't been deleted or moved * Check API response for any error messages about the visual **Problem:** Intro or outro displays for incorrect amount of time. **Solution:** * Check the `minimumDuration` value is in seconds, not milliseconds * Verify you set the intended duration (e.g., 5 for 5 seconds) * For video backgrounds, duration cannot exceed the video file's length * Scene will use the longer of `minimumDuration` or actual video length * Test different duration values to find the right timing * Consider platform norms (social media = shorter, YouTube = longer) **Problem:** Intro image appears blurry or low quality in final video. **Solution:** * Use high-resolution images (minimum 1920x1080 for Full HD) * Avoid upscaling small images - create them at target resolution * Use PNG format for graphics and logos (lossless) * For JPEG, use high quality settings (90%+ quality) * Check source image is not already low quality * Ensure image aspect ratio matches video (16:9 for most platforms) * Test with different image files to isolate the issue **Problem:** Outro video clip plays without sound in final video. **Solution:** * Background videos in intro/outro scenes may not include audio by default * Add background music separately using the `backgroundMusic` parameter * Verify outro video file actually contains audio track * Check if audio is being mixed correctly with other audio layers * Consider that outro visuals and audio may be handled separately * Use background music for consistent audio across all scenes **Problem:** Intro appears at end, or scenes are in wrong order. **Solution:** * Scenes appear in the order they are listed in the `scenes` array * Ensure intro scene is first in the array * Ensure outro scene is last in the array * Content scenes go between intro and outro * Double-check array order in your code * Example order: `[intro_scene, content_scene, outro_scene]` **Problem:** API cannot download your intro image or outro video. **Solution:** * Verify files are publicly accessible (test URL in incognito browser) * Check file sharing permissions are set correctly * Ensure URLs are direct download links, not web pages * For cloud storage, verify sharing settings allow public access * Test URL with curl or browser to confirm it downloads the file * Check for expired sharing links or restricted access * Try uploading to different hosting service if issues persist ## Advanced Configurations ### Multiple Content Scenes with Intro/Outro ```javascript theme={null} { scenes: [ // Intro { background: { type: "image", visualUrl: "..." }, minimumDuration: 5 }, // Multiple content scenes { story: "First section...", createSceneOnNewLine: true }, { story: "Second section...", createSceneOnEndOfSentence: true }, { story: "Third section...", createSceneOnNewLine: true }, // Outro { background: { type: "video", visualUrl: "..." }, minimumDuration: 8 } ] } ``` ### Intro/Outro with Background Music ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "https://storage.example.com/background-music.mp3", volume: 0.3 }, scenes: [ // Intro scene { background: { type: "image", visualUrl: "..." }, minimumDuration: 5 }, // Content { story: "...", createSceneOnNewLine: true }, // Outro scene { background: { type: "video", visualUrl: "..." }, minimumDuration: 7 } ] } ``` ### Intro/Outro with Branding ```javascript theme={null} { brandName: "Your Brand Name", // Apply brand settings scenes: [ // Intro { background: { type: "image", visualUrl: "..." }, minimumDuration: 4 }, // Content (branded) { story: "...", createSceneOnNewLine: true }, // Outro { background: { type: "video", visualUrl: "..." }, minimumDuration: 6 } ] } ``` ## Next Steps Enhance your intro/outro videos with these complementary features: Apply consistent branding to all video elements Add music to complement intro/outro scenes Add logo watermarks to your videos Style subtitles to match your brand ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including scene configuration * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Video Intro and Outro Source: https://docs.pictory.ai/guides/advanced-features/intro-outro-video Add video-based intro and outro sequences with automatic duration detection This guide shows you how to add professional video-based intro and outro sequences to your videos. Video backgrounds automatically detect duration, making it easy to add branded sequences without manual timing configuration. ## What You'll Learn Add video-based branded intro and outro sequences Automatically use video's actual duration without manual configuration Optionally override duration to trim or loop videos Control audio playback with mute and loop settings ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Intro and outro video URLs (MP4 format recommended) * Videos are high quality (1920x1080 or higher recommended) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Video Intro/Outro Works When you add video intro/outro sequences: 1. **Intro Scene Definition** - Create first scene with video background pointing to intro URL 2. **Duration Detection** - System automatically detects video duration from file 3. **Duration Override (Optional)** - Add `minimumDuration` to trim or loop to specific length 4. **Main Content Addition** - Include your main content scenes after intro 5. **Outro Scene Definition** - Create last scene with video background pointing to outro URL 6. **Audio Settings Application** - Mute and loop settings are applied to intro/outro videos 7. **Video Rendering** - Final video rendered with intro, content, and outro For intro and outro videos, omit `minimumDuration` to use the video's actual duration. This ensures your branded sequences play completely without trimming. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT = "AI is poised to significantly impact educators and course creators."; const INTRO_VIDEO_URL = "https://pictory-static.pictorycontent.com/PictoryLogoINTRO.mp4"; const OUTRO_VIDEO_URL = "https://pictory-static.pictorycontent.com/PictoryLogoOUTRO.mp4"; async function createTextToVideoWithIntroOutro() { try { console.log("Creating video with intro and outro..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_intro_outro", scenes: [ // Intro scene - with optional minimumDuration { background: { type: "video", visualUrl: INTRO_VIDEO_URL, }, minimumDuration: 2, // Optional: override video duration }, // Main content { story: STORY_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, // Outro scene - without minimumDuration (auto-detect) { background: { type: "video", visualUrl: OUTRO_VIDEO_URL, }, // minimumDuration omitted - uses actual video duration }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video with intro and outro render job created!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with intro and outro is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createTextToVideoWithIntroOutro(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT = "AI is poised to significantly impact educators and course creators." INTRO_VIDEO_URL = "https://pictory-static.pictorycontent.com/PictoryLogoINTRO.mp4" OUTRO_VIDEO_URL = "https://pictory-static.pictorycontent.com/PictoryLogoOUTRO.mp4" def create_text_to_video_with_intro_outro(): try: print("Creating video with intro and outro...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_intro_outro', 'scenes': [ # Intro scene - with optional minimumDuration { 'background': { 'type': 'video', 'visualUrl': INTRO_VIDEO_URL }, 'minimumDuration': 2 # Optional: override video duration }, # Main content { 'story': STORY_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True }, # Outro scene - without minimumDuration (auto-detect) { 'background': { 'type': 'video', 'visualUrl': OUTRO_VIDEO_URL } # minimumDuration omitted - uses actual video duration } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video with intro and outro render job created!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with intro and outro is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_text_to_video_with_intro_outro() ``` ## Understanding the Parameters ### Video Background Parameters | Parameter | Type | Default | Description | | -------------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------- | | `background.type` | string | - | Must be `"video"` for video backgrounds | | `background.visualUrl` | string | - | URL to the video file (MP4 recommended) | | `background.settings.mute` | boolean | `true` | When `true`, mutes video audio. When `false`, includes video's original audio. | | `background.settings.loop` | boolean | `true` | When `true`, loops video if shorter than scene. When `false`, video plays once and freezes on last frame. | | `minimumDuration` | number | - | Optional. Scene duration in seconds. If omitted for videos, uses actual video duration. | ### Duration Behavior by Scenario | Scenario | minimumDuration | Behavior | | ------------------- | ------------------ | ------------------------------------------------------------------------ | | Custom video URL | Not provided | Uses actual video duration (auto-detected) ✅ Recommended for intro/outro | | Custom video URL | Provided (e.g., 3) | Video is trimmed or looped to match 3 seconds | | Stock library video | Provided | Video is trimmed or looped to match duration | | Image background | Required | Must specify duration (images have no inherent duration) | **Important:** For intro and outro videos, omit `minimumDuration` to use the video's full duration. This ensures your branded sequences play completely without trimming. ## Intro Duration Explained ### Without minimumDuration (Recommended) ```javascript theme={null} { background: { type: "video", visualUrl: INTRO_VIDEO_URL } // minimumDuration omitted - uses actual video duration } ``` **When to use:** * Want full branded intro sequence to play * Video duration is already optimal length * Don't want to trim or loop the video * Most common use case for intro/outro **Result:** Video plays for its full duration automatically. ### With minimumDuration Override ```javascript theme={null} { background: { type: "video", visualUrl: INTRO_VIDEO_URL }, minimumDuration: 3 // Override to 3 seconds } ``` **When to use:** * Need to trim long intro to specific duration * Want consistent timing across multiple videos * Need to loop short video to fill time **Result:** Video is trimmed or looped to match 3 seconds. **Example:** 5-second intro with `minimumDuration: 3` plays only first 3 seconds. ## Outro Duration Explained ### Without minimumDuration (Recommended) ```javascript theme={null} { background: { type: "video", visualUrl: OUTRO_VIDEO_URL } // minimumDuration omitted - uses actual video duration } ``` **When to use:** * Want full branded outro sequence to play * Outro contains important info (CTAs, contact info) * Video duration is already optimal length * Most common use case for outro **Result:** Video plays for its full duration automatically. ### With minimumDuration Override ```javascript theme={null} { background: { type: "video", visualUrl: OUTRO_VIDEO_URL }, minimumDuration: 8 // Override to 8 seconds } ``` **When to use:** * Need to trim long outro to specific duration * Want consistent timing across multiple videos * Social media platforms have time constraints **Result:** Video is trimmed or looped to match 8 seconds. **Best Practice:** Keep intro videos short (2-5 seconds) to maintain viewer attention. Outro videos can be longer (5-10 seconds) to include calls-to-action and contact information. ## Duration Best Practices ### Recommended Intro Durations | Duration | Use Case | Platform | Example | | ----------- | -------------------------- | ------------------- | ------------------------- | | 2-3 seconds | Quick logo reveal | Social media | Brand logo animation | | 3-5 seconds | Branded intro with tagline | Corporate videos | Company name + slogan | | 5-8 seconds | Full branded sequence | Educational content | Logo + tagline + music | | 8+ seconds | ⚠️ Too long | Not recommended | May lose viewer attention | ### Recommended Outro Durations | Duration | Use Case | Platform | Example | | ------------- | ------------ | --------------------- | ----------------------------------- | | 5-8 seconds | Simple CTA | Social media | "Subscribe" message with logo | | 8-12 seconds | Detailed CTA | Corporate/Educational | Subscribe + social links + website | | 12-15 seconds | Extended CTA | Long-form content | Full contact info + multiple CTAs | | 15+ seconds | ⚠️ Too long | Not recommended | Viewers may leave before completion | ## Video Background with Audio Settings Control audio playback in your intro and outro videos: ```javascript theme={null} { background: { type: "video", visualUrl: OUTRO_VIDEO_URL, settings: { mute: true, // Mute outro video audio (allows voiceover to be heard) loop: false // Don't loop the video (play once and freeze on last frame) } } } ``` **When to use mute:** * `mute: true` - When you have voiceover narration or want silent branded sequences * `mute: false` - When intro/outro has important music or sound effects **When to use loop:** * `loop: false` - For branded sequences that should play once (recommended for intro/outro) * `loop: true` - For background ambient videos in main content scenes ## Common Use Cases ### Corporate Video with Branded Intro/Outro ```javascript theme={null} { scenes: [ // Corporate intro with company branding { background: { type: "video", visualUrl: "https://your-cdn.com/corporate-intro.mp4", settings: { mute: false, // Include intro music loop: false // Play once only } } // minimumDuration omitted - uses actual video duration }, // Main content scenes { story: "Our quarterly results show significant growth..." }, // Corporate outro with contact information { background: { type: "video", visualUrl: "https://your-cdn.com/corporate-outro.mp4", settings: { mute: false, // Include outro music loop: false // Play once only } } } ] } ``` **Result:** Professional corporate video with branded intro/outro and company music. ### Educational Video with Quick Branding ```javascript theme={null} { scenes: [ // Short 2-second logo reveal { background: { type: "video", visualUrl: "https://your-cdn.com/edu-logo-intro.mp4" }, minimumDuration: 2 // Trim to exactly 2 seconds for quick intro }, // Educational content with voiceover { story: "Today we'll learn about photosynthesis...", voiceOver: { speaker: "Matthew" } }, // Outro with call-to-action { background: { type: "video", visualUrl: "https://your-cdn.com/edu-cta-outro.mp4" } // Uses full 8-second outro duration } ] } ``` **Result:** Quick branded intro, educational content with voice, full CTA outro. ### Social Media Video with Attention-Grabbing Intro ```javascript theme={null} { scenes: [ // Eye-catching 3-second intro { background: { type: "video", visualUrl: "https://your-cdn.com/social-hook-intro.mp4", settings: { mute: false, // Include energetic music loop: false } }, minimumDuration: 3 // Keep it short for social media }, // Main content { story: "5 tips to boost your productivity..." }, // Call-to-action outro { background: { type: "video", visualUrl: "https://your-cdn.com/social-cta-outro.mp4" }, minimumDuration: 5 // Short CTA for social media } ] } ``` **Result:** Fast-paced social media video with short intro/outro for platform best practices. ### Product Demo with Professional Bookends ```javascript theme={null} { scenes: [ // Professional intro with product branding { background: { type: "video", visualUrl: "https://your-cdn.com/product-intro.mp4", settings: { mute: false, loop: false } } }, // Product demonstration scenes { story: "Our new software streamlines your workflow..." }, // Outro with purchase information and discount code { background: { type: "video", visualUrl: "https://your-cdn.com/product-outro-with-discount.mp4", settings: { mute: false, loop: false } } // Uses full outro duration to show all purchase info } ] } ``` **Result:** Product demo with professional branded intro and detailed purchase outro. ## Best Practices Maintain viewer attention with brief intros: * **Social Media**: 2-3 seconds maximum * **Corporate Videos**: 3-5 seconds * **Educational Content**: 3-4 seconds * **Long Intros**: Increase drop-off rates significantly * **First Impression**: Viewers decide quickly whether to continue * **Test Variations**: A/B test different intro lengths Ensure professional appearance with quality files: * **Resolution**: 1920x1080 minimum for HD quality * **Format**: MP4 with H.264 codec for best compatibility * **Bitrate**: 5-10 Mbps for optimal quality/size balance * **Frame Rate**: 30fps or 60fps for smooth playback * **Aspect Ratio**: 16:9 for standard video players * **Brand Reflection**: Low-quality intro/outro reflects poorly on brand Ensure complete branded sequences: * **Branded Sequences**: Omit for full animation playback * **Outro with Info**: Ensure all contact info is visible * **CTA Videos**: Let complete CTA message play * **When to Include**: Only for trimming long videos * **Auto-Detection**: Let system use actual video duration * **Professional Finish**: Full sequences look more polished Manage audio layers for clear sound: * **With Voiceover**: Set `mute: true` on intro/outro * **Without Voiceover**: Set `mute: false` to include music * **Background Music**: Keep volume lower than voiceover * **Audio Conflicts**: Don't mix multiple audio sources * **Professional Mix**: Fewer audio layers = cleaner sound * **Test Output**: Preview to ensure audio balance Verify videos work correctly: * **URL Accessibility**: Video URL must be publicly accessible * **Browser Playback**: Video plays in browser without download * **Format Check**: Video is in MP4 format * **Aspect Ratio**: 16:9 recommended for standard players * **Audio Levels**: Audio is appropriate volume * **Test Render**: Create test video before bulk production ## Troubleshooting **Problem:** Video intro or outro does not show in rendered video. **Solution:** * Verify video URL is publicly accessible (test in browser) * Ensure video format is supported (use MP4 with H.264) * Check video file is not corrupted or incomplete * Verify URL uses HTTPS (not HTTP) * Test with known working video URL to isolate issue * Review API response for validation errors **Problem:** Video is trimmed and does not play fully. **Solution:** * Remove `minimumDuration` parameter to use full video duration * If duration must be specified, ensure it matches or exceeds video length * Check video's actual duration (right-click → Properties → Details) * Do not guess duration - let system auto-detect * Test without `minimumDuration` first * Only add `minimumDuration` when intentionally trimming **Problem:** Video repeats instead of playing once. **Solution:** * Set `settings.loop: false` explicitly in background configuration * Default is `loop: true`, must override for one-time playback * Remove `minimumDuration` to use actual video duration * If `minimumDuration` > video length and `loop: true`, video will loop * Verify loop setting is within scene's background configuration * Preview output to confirm loop behavior **Problem:** Intro/outro audio plays simultaneously with narration. **Solution:** * Set `settings.mute: true` to silence intro/outro audio * Use intro/outro videos without audio tracks * Adjust audio levels in source video files before upload * Do not mix background video audio + voiceover * Plan audio strategy before configuring settings * Test audio balance with actual voiceover **Problem:** Scene duration differs from what you expected. **Solution:** * Check actual video duration using media player * Re-encode video with standard settings (30fps, constant frame rate) * Use `minimumDuration` to explicitly set desired length * Verify video does not have variable frame rate (VFR) * Convert to constant frame rate (CFR) for predictable duration * Test with explicitly set `minimumDuration` **Problem:** Intro/outro appears pixelated or low quality. **Solution:** * Use 1920x1080 minimum resolution for source videos * Export videos at high bitrate (5-10 Mbps) * Use MP4 format with H.264 codec * Avoid re-compressing videos multiple times * Ensure aspect ratio matches project settings (16:9) * Test with high-quality source video to isolate issue ## Supported Video Formats Best practices for intro/outro video formats: | Format | Extension | Recommended | Notes | | ------ | --------- | ----------- | ---------------------------------- | | MP4 | `.mp4` | ✅ Yes | Best compatibility and compression | | WebM | `.webm` | ⚠️ Limited | May have compatibility issues | | MOV | `.mov` | ⚠️ Limited | Larger file sizes, use H.264 codec | | AVI | `.avi` | ❌ No | Not recommended, poor compression | **Recommended Format:** MP4 with H.264 codec at 1920x1080 resolution for best compatibility and quality across all platforms. ## Advanced Configuration ### Combining Intro/Outro with Other Features ```javascript theme={null} { scenes: [ // Intro with transition { background: { type: "video", visualUrl: INTRO_VIDEO_URL }, transition: { type: "fade", duration: 0.5 } }, // Main content with custom background and audio { story: "Your content here...", background: { type: "video", visualUrl: "https://your-cdn.com/background.mp4", settings: { mute: true, // Mute background to hear voiceover loop: true // Loop background video } }, voiceOver: { speaker: "Matthew" } }, // Outro with music and no transition { background: { type: "video", visualUrl: OUTRO_VIDEO_URL, settings: { mute: false, // Include outro music loop: false } } } ] } ``` **Result:** Smooth intro transition, looping background with voiceover, and musical outro. ## Next Steps Enhance your video intro/outro with these complementary features: Control mute and loop behavior for background videos Add smooth transitions between intro, content, and outro scenes Add professional voiceover to your videos Learn how to create main content scenes from text ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including intro/outro configuration * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Save as Editable Project Source: https://docs.pictory.ai/guides/advanced-features/save-project Save video storyboards as editable projects in your Pictory account for future editing and refinement This guide shows you how to create video storyboards and save them as editable projects in your Pictory account. Save projects to make changes later through the Pictory web interface, perfect for iterative workflows and collaborative video creation. ## What You'll Learn Save video storyboards as editable projects Access and modify projects through web interface Organize and manage saved projects Choose when to save vs. render directly ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Access to your Pictory account for viewing saved projects * Basic understanding of video storyboard creation ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Saving Projects Works When you save a video as an editable project: 1. **Storyboard Creation** - Your video storyboard is created with all scenes and settings 2. **Project Saving** - The complete project is saved to your Pictory account 3. **Video Rendering** - The video is rendered as normal (if using render endpoint) 4. **Account Storage** - Project appears in your "My Projects" section 5. **Web Access** - You can open and edit the project in Pictory's web editor 6. **Future Updates** - Make changes and re-render whenever needed Saved projects are stored in your Pictory account and count toward your account's project storage limit. You can access them anytime through the Pictory web interface at app.pictory.ai. ## Saved Projects vs. Direct Rendering ### With `saveProject: true` ✅ **Advantages:** * Project saved to your Pictory account * Full editing capability through web interface * Can modify scenes, visuals, text, and timing later * Perfect for iterative refinement and reviews * Enables collaborative workflows with team members * Create reusable templates for future videos ❌ **Considerations:** * Uses account project storage quota * Requires manual cleanup of old projects * Slightly longer processing time ### With `saveProject: false` (or omitted - default) ✅ **Advantages:** * Faster processing for one-time videos * No project storage used * Ideal for automated, high-volume workflows * Clean, streamlined API-only workflow ❌ **Considerations:** * Cannot edit through web interface later * No visual preview in Pictory account * Must recreate via API for any changes ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createAndSaveProject() { try { console.log("Creating video storyboard and saving as project..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "my_saved_project", // Project name in your account saveProject: true, // Save as editable project // Scene configuration scenes: [ { story: STORY_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video created and project saved!"); console.log("Video URL:", jobResult.data.videoURL); console.log("Project URL:", jobResult.data.projectUrl); console.log("\nAccess this project at: https://app.pictory.ai/"); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createAndSaveProject(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_and_save_project(): try: print("Creating video storyboard and saving as project...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'my_saved_project', # Project name in your account 'saveProject': True, # Save as editable project # Scene configuration 'scenes': [ { 'story': STORY_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video created and project saved!") print(f"Video URL: {job_result['data']['videoURL']}") print(f"Project URL: {job_result['data']['projectUrl']}") print("\nAccess this project at: https://app.pictory.ai/") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_and_save_project() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- | | `videoName` | string | Yes | Name of the project as it appears in your Pictory account. Use descriptive names for easy identification. | | `saveProject` | boolean | No | Set to `true` to save as editable project. Default is `false` (no project saved). | | `scenes` | array | Yes | Array of scene objects defining video content (same as normal video creation). | ### Response Fields (When Project Saved) | Field | Type | Description | | ------------ | ------ | ----------------------------------------------------- | | `jobId` | string | Unique identifier for tracking the job status | | `videoURL` | string | Direct download link to the rendered video file | | `projectUrl` | string | Link to open the project in Pictory's web editor | | `previewUrl` | string | Preview link for the video (accessible without login) | ## Accessing Saved Projects After creating a saved project, you can access it in several ways: ### Through Pictory Web Interface 1. Visit [https://app.pictory.ai/](https://app.pictory.ai/) 2. Log in to your Pictory account 3. Navigate to **My Projects** from the dashboard 4. Find your project by the `videoName` you specified 5. Click the project to open it in the visual editor 6. Make any changes (edit text, swap visuals, adjust timing, etc.) 7. Click **Generate Video** to re-render with your changes ### Through API Response When the job completes, the response includes: * `projectUrl` - Direct link to edit the project in Pictory * `previewUrl` - Shareable preview link for the video ### Organizing Projects Projects in your Pictory account can be: * **Renamed** - Change project names for better organization * **Duplicated** - Create copies for variations * **Deleted** - Remove old projects to free up storage * **Searched** - Find projects by name in the web interface * **Sorted** - Sort by creation date, name, or modification date ## Common Use Cases ### Collaborative Video Review ```javascript theme={null} { videoName: "marketing_video_draft_v1", saveProject: true, // Save for team review scenes: [ { story: "Your marketing message here...", createSceneOnNewLine: true } ] } ``` **Result:** Project saved for team members to review and refine through web interface before final approval. ### Template Creation ```javascript theme={null} { videoName: "weekly_update_template", saveProject: true, // Save as reusable template brandName: "Company Brand", scenes: [ { story: "This week's highlights and updates...", createSceneOnNewLine: true } ] } ``` **Result:** Create a template project that can be duplicated and customized weekly through the web interface. ### Iterative Refinement ```javascript theme={null} { videoName: "product_demo_v1", saveProject: true, // Save for multiple iterations scenes: [ { story: "Introducing our new product features...", createSceneOnEndOfSentence: true } ] } ``` **Result:** Generate initial version, then refine visuals, timing, and text through web editor based on feedback. ### Client Presentation Drafts ```javascript theme={null} { videoName: "client_proposal_acme_corp", saveProject: true, // Save for client changes scenes: [ { story: "Proposed campaign messaging and visuals...", createSceneOnNewLine: true } ] } ``` **Result:** Share preview with client, incorporate feedback by editing the saved project, then re-render final version. ## Best Practices Choose clear, organized naming conventions: * **Include Context**: "marketing\_q4\_social\_media" not "video1" * **Version Numbers**: "product\_launch\_v1", "product\_launch\_v2" * **Date Stamps**: "weekly\_update\_2024\_03\_15" * **Client/Project**: "acme\_corp\_proposal\_draft" * **Searchable**: Use keywords that make projects easy to find * **Consistent Format**: Establish naming conventions for your team Use saved projects for workflows involving multiple people: * **Team Review**: Let team members preview and suggest edits * **Client Approval**: Share preview link before finalizing * **Stakeholder Input**: Allow non-technical users to make changes * **Quality Control**: Enable review process before publication * **Iterative Feedback**: Make rounds of revisions through web interface Do not save projects for automated, one-time videos: * **Bulk Generation**: Creating hundreds of similar videos programmatically * **Automated Reports**: Daily/weekly videos that do not need editing * **Social Media Automation**: Scheduled posts generated automatically * **Data-Driven Videos**: Videos created from changing data sources * **Storage Management**: Avoid filling account storage with temporary projects Keep your account organized and under storage limits: * **Regular Cleanup**: Delete old projects you no longer need * **Archive Important Projects**: Download videos before deleting projects * **Monitor Storage**: Check account storage limits periodically * **Delete Duplicates**: Remove test projects and duplicates * **Use Descriptive Names**: Makes it easier to identify projects for deletion Saved projects work with all Pictory features: * **Brand Settings**: Apply brand presets to saved projects * **Custom Styles**: Use custom subtitle styles that can be edited later * **Voice-Over**: Add AI narration to saved projects * **Background Music**: Include music that can be changed in web editor * **Logos and Watermarks**: Add branding elements editable later ## Troubleshooting **Problem:** Created project with `saveProject: true` but cannot find it in Pictory account. **Solution:** * Wait for job to complete - projects appear only after "completed" status * Check you are logged into the correct Pictory account (same as API key) * Search for the project using the exact `videoName` you specified * Sort by "Date Created" to see most recent projects first * Refresh the My Projects page in your browser * Check if job actually succeeded (status should be "completed", not "failed") **Problem:** Project appears in account but editing options are limited. **Solution:** * Ensure you have an active Pictory subscription with editing privileges * Some features may require specific subscription tiers * Try refreshing the page or clearing browser cache * Check if the project fully loaded before making edits * Verify account permissions if using a team account **Problem:** Error message about reaching project storage limit. **Solution:** * Delete old projects you no longer need from My Projects * Download important videos before deleting their projects * Consider upgrading to a plan with higher storage limits * Use `saveProject: false` for videos that do not need editing * Regularly clean up test projects and duplicates * Archive completed projects by downloading videos and deleting projects **Problem:** Made edits to project but changes are not persisting. **Solution:** * Click the "Save" or "Update" button in the web editor * Wait for save confirmation message before closing the browser * Check your internet connection while editing * Avoid making edits in multiple browser tabs simultaneously * Refresh the page and check if changes were saved * Try editing again if changes were lost **Problem:** The `projectUrl` from API response shows "Not Found" error. **Solution:** * Ensure you are logged into Pictory before clicking the project URL * Check that the job completed successfully (status = "completed") * Copy the full URL including all parameters * Try accessing through My Projects instead of direct URL * Wait a few minutes for project indexing to complete * Contact support if problem persists with job ID **Problem:** Got video output but project does not appear in account. **Solution:** * Verify you included `saveProject: true` in the request payload * Check the parameter is boolean `true`, not string `"true"` * Ensure parameter is at the top level, not nested in scenes * Review the request payload you sent to confirm parameter presence * Try the request again with explicit `saveProject: true` * Check API response for any warnings about saving ## API Endpoints for Saved Projects ### Storyboard Endpoint (Saves but Does Not Render) ```javascript theme={null} POST /v2/video/storyboard { "videoName": "project_name", "saveProject": true, "scenes": [...] } ``` **Use Case:** Create editable project without rendering video yet. Faster processing, edit first in web interface, then render. ### Render Endpoint (Saves and renders) ```javascript theme={null} POST /v2/video/storyboard/render { "videoName": "project_name", "saveProject": true, "scenes": [...] } ``` **Use Case:** Create project and render video in one step. Gets both editable project and finished video. **Choosing the Right Endpoint:** * Use `/storyboard` if you want to edit the project in the web interface before rendering * Use `/storyboard/render` if you want both a rendered video and the ability to edit later * Omit `saveProject` (or set to `false`) for either endpoint if you only need the video output ## Next Steps Enhance your saved projects workflow with these features: Apply consistent branding to all your projects Create custom subtitle styles for your projects Add background music to your saved projects Include intro and outro sequences in projects ## API Reference For complete technical details, see: Create editable project without rendering Get projectId, projectUrl, and preview results Create project and render video Render an existing saved project Monitor render progress and get video URLs # Scene-Level Music Control Source: https://docs.pictory.ai/guides/advanced-features/scene-music Enable or disable background music for specific scenes to create dynamic audio experiences in your videos This guide shows you how to control background music at the scene level. Enable or disable music for individual scenes within the same video to create dynamic audio experiences, emphasize key moments, or add variety to your content. ## What You'll Learn Set default background music for entire video Enable or disable music for specific scenes Create videos with dynamic music control Design effective scene-by-scene audio ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Understanding of video-level background music configuration * Basic knowledge of scene structure in Pictory API ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Scene-Level Music Control Works When you configure music at the scene level: 1. **Video-Level Default** - Define default background music settings for entire video 2. **Scene Processing** - Each scene is evaluated for scene-level overrides 3. **Override Detection** - System checks if scene has `backgroundMusic.enabled` setting 4. **Music Application** - Scenes use video-level music unless explicitly overridden 5. **Audio Mixing** - Music is mixed with voice-over and other audio per scene 6. **Transition Handling** - Smooth transitions between scenes with/without music 7. **Video Rendering** - Final video rendered with scene-specific music control Scene-level control only allows enabling or disabling music. All scenes that have music enabled use the same music track defined at the video level. You cannot use different music tracks for different scenes. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media."; const STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing."; const STORY_TEXT_3 = "AI will save time and enhance consistency for content creators everywhere."; async function createVideoWithSceneMusicControl() { try { console.log("Creating video with scene-level music control..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_scene_music_control", // VIDEO-LEVEL: Default background music for all scenes backgroundMusic: { enabled: true, // Enable by default autoMusic: true, // AI-selected music volume: 0.3, // 30% volume }, scenes: [ // SCENE 1: Uses video-level music (default) { story: STORY_TEXT_1, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // No scene-level override - music plays }, // SCENE 2: Disable music for this specific scene { story: STORY_TEXT_2, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, backgroundMusic: { enabled: false, // Override: disable music }, }, // SCENE 3: Uses video-level music again (default) { story: STORY_TEXT_3, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // No scene-level override - music plays }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with scene-level music control is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithSceneMusicControl(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT_1 = ( "AI is poised to significantly impact educators and course creators on social media." ) STORY_TEXT_2 = ( "By automating tasks like content generation, visual design, and video editing." ) STORY_TEXT_3 = ( "AI will save time and enhance consistency for content creators everywhere." ) def create_video_with_scene_music_control(): try: print("Creating video with scene-level music control...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_scene_music_control', # VIDEO-LEVEL: Default background music for all scenes 'backgroundMusic': { 'enabled': True, # Enable by default 'autoMusic': True, # AI-selected music 'volume': 0.3 # 30% volume }, 'scenes': [ # SCENE 1: Uses video-level music (default) { 'story': STORY_TEXT_1, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False # No scene-level override - music plays }, # SCENE 2: Disable music for this specific scene { 'story': STORY_TEXT_2, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'backgroundMusic': { 'enabled': False # Override: disable music } }, # SCENE 3: Uses video-level music again (default) { 'story': STORY_TEXT_3, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False # No scene-level override - music plays } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with scene-level music control is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_scene_music_control() ``` ## Understanding the Parameters ### Video-Level Background Music | Parameter | Type | Required | Description | | --------------------------- | ------- | -------- | ------------------------------------------------------------------ | | `backgroundMusic.enabled` | boolean | Yes | Set to `true` to enable background music by default for all scenes | | `backgroundMusic.musicUrl` | string | No | Custom music URL (or use `autoMusic` instead) | | `backgroundMusic.autoMusic` | boolean | No | Use AI-selected music | | `backgroundMusic.volume` | number | No | Volume level (0-1). Default: 0.5 | ### Scene-Level Override | Parameter | Type | Required | Description | | ---------------------------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `scenes[].backgroundMusic.enabled` | boolean | No | Override video-level setting. `true` = enable music for this scene, `false` = disable music for this scene | **Scene-Level Limitations:** * Scene-level `backgroundMusic` only accepts the `enabled` parameter (true/false) * You cannot change music track, volume, or clips at scene level * All scenes with music enabled use the same track from video-level configuration * To use different music tracks, create separate videos ## Music Control Patterns ### Pattern 1: Music On Most Scenes, Off for Key Moments Use music throughout, but disable it for important announcements or dramatic pauses. ```javascript theme={null} { // Video-level: Music enabled by default backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, scenes: [ { story: "Welcome to our presentation..." }, // ✓ Music ON { story: "Today we'll cover three topics..." }, // ✓ Music ON { story: "IMPORTANT: Please note this key point...", backgroundMusic: { enabled: false } // ✗ Music OFF }, { story: "Continuing with our discussion..." }, // ✓ Music ON { story: "Call to action: Visit our website now!", backgroundMusic: { enabled: false } // ✗ Music OFF } ] } ``` **Result:** Music plays throughout except during critical messages for maximum clarity. ### Pattern 2: Music Only on Specific Scenes Start with no music by default, enable only for specific scenes. ```javascript theme={null} { // Video-level: Music disabled by default backgroundMusic: { enabled: false, musicUrl: "https://example.com/upbeat-music.mp3", volume: 0.5 }, scenes: [ { story: "Intro scene with energy!", backgroundMusic: { enabled: true } // ✓ Music ON }, { story: "Main content without distraction..." }, // ✗ Music OFF { story: "Detailed explanation..." }, // ✗ Music OFF { story: "Technical information..." }, // ✗ Music OFF { story: "Exciting outro! Subscribe now!", backgroundMusic: { enabled: true } // ✓ Music ON } ] } ``` **Result:** Music plays only during intro and outro, main content is music-free. ### Pattern 3: Alternating Music for Variety Alternate music on/off to create rhythm and variety. ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.4 }, scenes: [ { story: "First point with music..." }, // ✓ Music ON { story: "Second point - focus moment", backgroundMusic: { enabled: false } // ✗ Music OFF }, { story: "Third point with music..." }, // ✓ Music ON { story: "Fourth point - another focus moment", backgroundMusic: { enabled: false } // ✗ Music OFF }, { story: "Conclusion with music..." } // ✓ Music ON ] } ``` **Result:** Creates dynamic pacing with alternating music and silence. ### Pattern 4: Music Only During Transitions Use music only between content sections for smooth transitions. ```javascript theme={null} { backgroundMusic: { enabled: false, autoMusic: true, volume: 0.3 }, scenes: [ { story: "Section 1: Introduction..." }, // ✗ Music OFF { story: "", background: { type: "image", visualUrl: "transition.png" }, minimumDuration: 2, backgroundMusic: { enabled: true } // ✓ Music ON (transition) }, { story: "Section 2: Main content..." }, // ✗ Music OFF { story: "", background: { type: "image", visualUrl: "transition.png" }, minimumDuration: 2, backgroundMusic: { enabled: true } // ✓ Music ON (transition) }, { story: "Section 3: Conclusion..." } // ✗ Music OFF ] } ``` **Result:** Music plays only during transition scenes for smooth section changes. ## Common Use Cases ### Educational Videos - Focus on Key Concepts ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.25 }, scenes: [ { story: "Today's lesson is about quantum physics..." }, { story: "Here's the key formula you need to remember...", backgroundMusic: { enabled: false } // No music for important formula }, { story: "Now let's see how this applies..." }, { story: "This critical concept is the foundation...", backgroundMusic: { enabled: false } // No music for critical concept } ] } ``` **Result:** Subtle background music except during key learning moments for better retention. ### Marketing Videos - Emphasize Call-to-Action ```javascript theme={null} { backgroundMusic: { enabled: true, musicUrl: "upbeat-corporate.mp3", volume: 0.5 }, scenes: [ { story: "Introducing our revolutionary new product..." }, { story: "Features include advanced AI technology..." }, { story: "Limited time offer! Get 50% off today only!", backgroundMusic: { enabled: false } // Silence for CTA impact } ] } ``` **Result:** Energetic music builds excitement, then silence makes CTA stand out. ### Podcast/Interview Clips - Highlight Quotes ```javascript theme={null} { backgroundMusic: { enabled: false }, scenes: [ { story: "Intro sequence", backgroundMusic: { enabled: true } // Music for intro }, { story: "Our guest shares their expertise..." }, // No music during talk { story: "Interview question and answer..." }, // No music during talk { story: "Outro and closing remarks", backgroundMusic: { enabled: true } // Music for outro } ] } ``` **Result:** Music bookends the interview, content is clear and uninterrupted. ### Testimonial Videos - Authentic Voice ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, scenes: [ { story: "Real customers share their experiences..." }, { story: "This product completely changed my business...", backgroundMusic: { enabled: false } // No music for testimonial }, { story: "I saw results within the first week...", backgroundMusic: { enabled: false } // No music for testimonial }, { story: "Join thousands of satisfied customers today..." } ] } ``` **Result:** Authentic testimonials stand out without music, framing content has music. ## Best Practices Use silence strategically to emphasize critical content: * **Key Announcements**: Turn off music for important news or updates * **Call-to-Action**: Disable music during CTA for maximum impact * **Statistics/Data**: Present numbers and facts without musical distraction * **Testimonials**: Let authentic voices speak without background music * **Complex Concepts**: Turn off music when explaining difficult topics * **Legal/Disclaimers**: Ensure disclaimers are heard clearly without music Enable music strategically to enhance engagement: * **Intro/Outro**: Use music to bookend your content professionally * **Topic Transitions**: Add music during section changes for smooth flow * **Upbeat Moments**: Enable music for exciting or celebratory content * **Background Context**: Use subtle music during supporting information * **B-Roll Sequences**: Add music during visual-only scenes * **Energy Boost**: Turn music on to re-engage viewers during longer content Alternate music on/off to maintain viewer interest: * **Vary Audio Landscape**: Don't use the same pattern throughout * **Match Content Mood**: Enable music for lighter topics, disable for serious ones * **Prevent Fatigue**: Alternating helps prevent audio monotony * **Scene Variety**: Mix music/no-music scenes for dynamic feel * **Rhythm Creation**: Establish patterns viewers can anticipate * **Strategic Silence**: Use silence as a tool, not just absence of sound Balance music with narration for clarity: * **With Heavy Narration**: Consider disabling music for content-heavy scenes * **Short Voice-Over**: Music works well with brief narration * **Technical Narration**: Disable music when explaining complex procedures * **Volume Balance**: Even when music is on, keep it low (0.2-0.3) with voice-over * **Speaker Changes**: Consider music changes when speakers change * **Emphasis Through Contrast**: Use music/silence to emphasize speaker's words Always preview to ensure proper music control: * **Listen Carefully**: Review the full video to check music transitions * **Check Transitions**: Ensure smooth audio when music starts/stops * **Volume Levels**: Verify music does not overpower when enabled * **Scene Timing**: Confirm music changes align with scene boundaries * **Viewer Perspective**: Consider if music changes make sense to viewers * **Iterate**: Adjust scene-level settings based on preview feedback ## Troubleshooting **Problem:** Scene still has music even though `enabled: false` was set. **Solution:** * Verify you set `backgroundMusic: { enabled: false }` at scene level * Check JSON syntax is correct (proper nesting and commas) * Ensure video-level music is enabled first (nothing to override otherwise) * Confirm scene-level setting is within the specific scene object * Review API response for any validation errors * Test with a simple 2-scene example to isolate the issue **Problem:** Want different music for different scenes, but it is not working. **Solution:** * Scene-level control only allows enabling/disabling, not changing tracks * All scenes with music use the video-level track configuration * To use different music tracks: * Create separate videos for each music track needed * Use video editing software for multi-track music after export * Consider using different clips from the same track (video-level clips) * This is a current limitation of the scene-level music control feature **Problem:** Music starts/stops suddenly between scenes, sounds jarring. **Solution:** * This is expected behavior - music transitions align with scene boundaries * The API automatically applies subtle fade in/out at scene transitions * For smoother transitions, consider: * Adjusting scene durations to align with musical phrases * Using longer scenes where music stays consistent * Choosing background music with gentle instrumentation * Very short scenes may have more noticeable transitions **Problem:** Scene-level `enabled: false` seems to be ignored. **Solution:** * Verify video-level music is properly configured first * Check that scene-level override is `backgroundMusic: { enabled: false }` * Ensure you are not just setting `enabled: false` at video level * Confirm proper JSON structure and nesting * Example correct structure: ```javascript theme={null} { backgroundMusic: { enabled: true, ... }, // Video level scenes: [ { story: "...", backgroundMusic: { enabled: false } } // Scene level ] } ``` **Problem:** Set video-level music to disabled, but want to enable for one scene. **Solution:** * This is supported - set video-level to `enabled: false` * Then enable for specific scenes with `backgroundMusic: { enabled: true }` * Ensure video-level still has `musicUrl` or `autoMusic` configured * Even with `enabled: false` at video level, you need music source defined * Example: ```javascript theme={null} { backgroundMusic: { enabled: false, autoMusic: true, volume: 0.3 }, scenes: [ { story: "...", backgroundMusic: { enabled: true } } // Override: ON ] } ``` **Problem:** Want different volume levels for different scenes. **Solution:** * Scene-level control does not support volume adjustments * Only `enabled` (true/false) can be set at scene level * All scenes use the video-level volume setting * Workaround options: * Use audio editing software post-export for scene-specific volume * Choose background music with natural volume variations * Create separate videos if drastically different volumes needed * This is a current limitation of scene-level music control ## Hierarchy and Override Rules Understanding how video-level and scene-level settings interact: ### Default Behavior (No Scene Override) ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, scenes: [ { story: "Scene 1" }, // Uses video-level: Music ON, Volume 0.3 { story: "Scene 2" }, // Uses video-level: Music ON, Volume 0.3 { story: "Scene 3" } // Uses video-level: Music ON, Volume 0.3 ] } ``` ### With Scene-Level Override ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, scenes: [ { story: "Scene 1" }, // Music ON { story: "Scene 2", backgroundMusic: { enabled: false } }, // Music OFF (override) { story: "Scene 3" } // Music ON ] } ``` ### Video-Level Disabled, Scene-Level Enabled ```javascript theme={null} { backgroundMusic: { enabled: false, autoMusic: true, volume: 0.4 }, scenes: [ { story: "Scene 1" }, // Music OFF { story: "Scene 2", backgroundMusic: { enabled: true } }, // Music ON (override) { story: "Scene 3" } // Music OFF ] } ``` ## Next Steps Enhance your scene-level music control with these complementary features: Learn about video-level background music configuration Add narration that works with scene music Combine scene music control with intro/outro Style subtitles for scenes with/without music ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including scene-level configuration * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Scene Transitions Source: https://docs.pictory.ai/guides/advanced-features/transitions Add smooth transition effects between scenes to create professional, polished videos with visual continuity This guide shows you how to add transition effects between scenes in your videos. Transitions provide smooth visual changes when moving from one scene to another, creating professional polish and visual flow that keeps viewers engaged. ## What You'll Learn Choose from 10+ professional transition effects Set different transitions for each scene Create smooth flow between scenes Enhance videos with polished transitions ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Multiple scenes in your video (transitions connect scenes) * Basic understanding of scene configuration in Pictory API ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Scene Transitions Work When you add transitions to your video: 1. **Transition Selection** - You specify a transition type for each scene 2. **Scene Boundary Detection** - System identifies where scenes connect 3. **Transition Placement** - Transition is applied at the end of each scene 4. **Effect Application** - Visual effect is rendered between scenes 5. **Duration Calculation** - Transition duration is automatically optimized 6. **Scene Stitching** - Scenes are seamlessly connected with transitions 7. **Video Rendering** - Final video includes all transitions between scenes Each scene's `sceneTransition` parameter defines the transition effect that plays **when moving TO the next scene**. The last scene's transition is typically not used since there is no scene after it. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media."; const STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing."; const STORY_TEXT_3 = "AI will save time and enhance consistency for content creators everywhere."; async function createVideoWithTransitions() { try { console.log("Creating video with scene transitions..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_scene_transitions", scenes: [ // SCENE 1: Fade transition to Scene 2 { story: STORY_TEXT_1, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, sceneTransition: "fade", // Classic fade effect }, // SCENE 2: Wipe right transition to Scene 3 { story: STORY_TEXT_2, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, sceneTransition: "wiperight", // Wipe moving right }, // SCENE 3: No transition (last scene) { story: STORY_TEXT_3, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, sceneTransition: "none", // Hard cut (or omit) }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with scene transitions is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithTransitions(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT_1 = ( "AI is poised to significantly impact educators and course creators on social media." ) STORY_TEXT_2 = ( "By automating tasks like content generation, visual design, and video editing." ) STORY_TEXT_3 = ( "AI will save time and enhance consistency for content creators everywhere." ) def create_video_with_transitions(): try: print("Creating video with scene transitions...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_scene_transitions', 'scenes': [ # SCENE 1: Fade transition to Scene 2 { 'story': STORY_TEXT_1, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'sceneTransition': 'fade' # Classic fade effect }, # SCENE 2: Wipe right transition to Scene 3 { 'story': STORY_TEXT_2, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'sceneTransition': 'wiperight' # Wipe moving right }, # SCENE 3: No transition (last scene) { 'story': STORY_TEXT_3, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'sceneTransition': 'none' # Hard cut (or omit) } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with scene transitions is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_transitions() ``` ## Understanding the Parameters ### Scene Transition Configuration | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | | `sceneTransition` | string | No | Transition effect applied when moving to the next scene. See available transitions below. | **Transition Direction:** The `sceneTransition` on Scene 1 defines the transition FROM Scene 1 TO Scene 2. The transition is placed at the END of the scene, not the beginning. The last scene's transition is not used. ## Available Transitions ### Basic Transitions | Transition | Effect Description | Visual Result | Best Used For | | ---------- | --------------------------- | ---------------------- | -------------------------------------- | | `none` | No transition, direct cut | Immediate scene change | Fast-paced content, news, tutorials | | `fade` | Classic fade to black/white | Gradual fade out/in | Professional videos, all content types | ### Directional Wipe Transitions | Transition | Effect Description | Visual Result | Best Used For | | ----------- | ------------------------ | ----------------------------- | ------------------------------------- | | `wipeup` | Scene wipes upward | New scene reveals from bottom | Uplifting content, positive messages | | `wipedown` | Scene wipes downward | New scene reveals from top | Serious topics, transitions to detail | | `wipeleft` | Scene wipes to the left | New scene pushes from right | Forward progression, next steps | | `wiperight` | Scene wipes to the right | New scene pushes from left | Going back, flashbacks, recaps | ### Smooth Slide Transitions | Transition | Effect Description | Visual Result | Best Used For | | ------------- | ------------------------- | ---------------------------- | --------------------------------------- | | `smoothleft` | Smooth slide to the left | Seamless horizontal movement | Sequential content, step-by-step guides | | `smoothright` | Smooth slide to the right | Seamless horizontal movement | Navigation, menu-style presentations | ### Special Effect Transitions | Transition | Effect Description | Visual Result | Best Used For | | ------------ | -------------------------------- | ----------------------------- | --------------------------------------- | | `radial` | Radial/circular wipe from center | Expanding circle reveal | Focus points, reveals, dramatic moments | | `circlecrop` | Circle crop effect | Circular mask transition | Spotlight content, modern aesthetics | | `hblur` | Horizontal motion blur | Blurred horizontal transition | Dynamic content, modern/stylish videos | ## Transition Selection Guide Choose transitions based on your content type and brand style: | Content Type | Recommended Transitions | Reasoning | | -------------------------- | ----------------------------------- | -------------------------------------------- | | **Professional/Corporate** | `fade`, `none` | Clean, conservative, universally appropriate | | **Educational/Tutorial** | `fade`, `smoothright`, `none` | Clear, non-distracting, maintains focus | | **Marketing/Sales** | `wiperight`, `fade`, `radial` | Dynamic, engaging, draws attention | | **Social Media** | `hblur`, `wiperight`, `circlecrop` | Modern, energetic, eye-catching | | **News/Information** | `none`, `fade` | Fast-paced, professional, straightforward | | **Storytelling/Narrative** | `fade`, `smoothleft`, `smoothright` | Smooth flow, maintains narrative continuity | | **Product Demos** | `wiperight`, `smoothright`, `fade` | Shows progression, highlights features | | **Testimonials** | `fade`, `none` | Professional, focuses on speaker | ## Transition Patterns ### Pattern 1: Consistent Transitions Throughout Use the same transition for all scenes for cohesive feel. ```javascript theme={null} { scenes: [ { story: "Scene 1 content...", sceneTransition: "fade" }, { story: "Scene 2 content...", sceneTransition: "fade" }, { story: "Scene 3 content...", sceneTransition: "fade" }, { story: "Scene 4 content...", sceneTransition: "fade" } ] } ``` **Result:** Consistent, professional feel with uniform transitions throughout. ### Pattern 2: Varied Transitions for Energy Mix different transitions to create dynamic pacing. ```javascript theme={null} { scenes: [ { story: "Introduction...", sceneTransition: "fade" }, { story: "Key point 1...", sceneTransition: "wiperight" }, { story: "Key point 2...", sceneTransition: "smoothleft" }, { story: "Conclusion...", sceneTransition: "fade" } ] } ``` **Result:** Dynamic video with varied visual interest. ### Pattern 3: No Transitions Except Section Breaks Use transitions only between major sections. ```javascript theme={null} { scenes: [ { story: "Intro scene 1...", sceneTransition: "none" }, { story: "Intro scene 2...", sceneTransition: "fade" }, // Section break { story: "Content scene 1...", sceneTransition: "none" }, { story: "Content scene 2...", sceneTransition: "none" }, { story: "Content scene 3...", sceneTransition: "fade" }, // Section break { story: "Conclusion...", sceneTransition: "none" } ] } ``` **Result:** Fast-paced content with transitions only marking major section changes. ### Pattern 4: Directional Storytelling Use directional transitions to show progression or flow. ```javascript theme={null} { scenes: [ { story: "Step 1...", sceneTransition: "wiperight" }, // Move forward { story: "Step 2...", sceneTransition: "wiperight" }, // Move forward { story: "Step 3...", sceneTransition: "wiperight" }, // Move forward { story: "Review...", sceneTransition: "wipeleft" }, // Go back { story: "Conclusion...", sceneTransition: "fade" } ] } ``` **Result:** Visual flow showing forward progression, then reflection. ## Common Use Cases ### Professional Presentations ```javascript theme={null} { scenes: [ { story: "Company overview and introduction...", sceneTransition: "fade" }, { story: "Our services and capabilities...", sceneTransition: "fade" }, { story: "Client success stories...", sceneTransition: "fade" }, { story: "Contact us for more information...", sceneTransition: "none" } ] } ``` **Result:** Clean, professional presentation with classic fade transitions. ### Social Media Content ```javascript theme={null} { scenes: [ { story: "Hook: Did you know this fact?", sceneTransition: "hblur" }, { story: "Main content and explanation...", sceneTransition: "wiperight" }, { story: "Call to action: Follow for more!", sceneTransition: "circlecrop" } ] } ``` **Result:** Dynamic, modern social media video with engaging transitions. ### Step-by-Step Tutorial ```javascript theme={null} { scenes: [ { story: "Step 1: First, do this...", sceneTransition: "smoothright" }, { story: "Step 2: Then, do that...", sceneTransition: "smoothright" }, { story: "Step 3: Next, complete this...", sceneTransition: "smoothright" }, { story: "Final result: You're done!", sceneTransition: "fade" } ] } ``` **Result:** Clear progression through steps with smooth directional transitions. ### Product Showcase ```javascript theme={null} { scenes: [ { story: "Introducing our new product...", sceneTransition: "radial" }, { story: "Feature 1: Advanced technology...", sceneTransition: "wiperight" }, { story: "Feature 2: Easy to use...", sceneTransition: "wiperight" }, { story: "Available now! Order today!", sceneTransition: "fade" } ] } ``` **Result:** Engaging product showcase with attention-grabbing transitions. ## Best Practices Choose transitions that complement your content style: * **Professional/Serious**: Stick to `fade` and `none` for conservative feel * **Energetic/Dynamic**: Use `wiperight`, `hblur`, `radial` for movement * **Modern/Stylish**: Try `circlecrop`, `hblur`, `smoothleft` * **Traditional/Classic**: Use `fade` exclusively for timeless look * **Educational**: Prefer subtle transitions (`fade`, `smoothright`) that do not distract * **Brand Alignment**: Ensure transitions match your brand personality Create visual consistency throughout your video: * **Same Transition**: Use one transition type for unified feel * **Transition Family**: If varying, stick to one family (all wipes, all smooth, etc.) * **Predictable Pattern**: Establish a pattern viewers can follow * **Section Markers**: Use special transitions only for major section changes * **Avoid Randomness**: Do not randomly mix transitions without purpose * **Test Consistency**: Preview to ensure transitions feel cohesive Exercise restraint with flashy effects: * **Less is More**: Dramatic transitions lose impact if overused * **Strategic Placement**: Save dramatic transitions for key moments * **Viewer Fatigue**: Too many effects can tire viewers * **Content Focus**: Transitions should enhance, not overshadow content * **Professional Standard**: Corporate content should favor subtle transitions * **Test Audience**: Check if transitions distract from message Ensure transitions fit your video pacing: * **Scene Length**: Very short scenes with transitions may feel rushed * **Content Type**: Fast-paced content works with quick transitions (none, fade) * **Viewer Comfort**: Give viewers time to process each scene change * **Music Sync**: Consider how transitions work with background music * **Natural Breaks**: Place transitions at natural content breaks * **Flow Testing**: Preview to ensure pacing feels natural Choose transitions based on where and to whom you are publishing: * **YouTube**: `fade` and `wiperight` work well for most content * **Instagram/TikTok**: Modern transitions (`hblur`, `circlecrop`) for younger audience * **LinkedIn**: Conservative transitions (`fade`, `none`) for professional network * **Corporate**: Stick to `fade` for internal/external business videos * **Educational**: Subtle transitions that do not distract learners * **Platform Trends**: Research what is popular on your target platform ## Troubleshooting **Problem:** Expected transition effect does not show between scenes. **Solution:** * Verify you set `sceneTransition` on the FIRST scene (not the second) * Remember: Scene 1's transition goes TO Scene 2 * Check spelling of transition name (case-sensitive) * Valid transitions: `fade`, `none`, `wipeup`, `wipedown`, `wipeleft`, `wiperight`, `smoothleft`, `smoothright`, `radial`, `circlecrop`, `hblur` * Ensure you have at least 2 scenes (transitions connect scenes) * Last scene's transition typically is not used (no scene after it) **Problem:** Transition effect shows up between unexpected scenes. **Solution:** * Review which scene has the `sceneTransition` parameter * Transition is placed at the END of the scene that defines it * If Scene 2 has `sceneTransition: "fade"`, it affects Scene 2 → Scene 3 * Check your scene array order matches your intended flow * Verify you didn't accidentally add transition to wrong scene * Use comments in code to label which transition goes where **Problem:** Different transition types look very similar in output. **Solution:** * Some transitions are subtle (fade vs. none can be quick) * Preview the video carefully to see transition effects * Very short scenes may not show full transition effect * Try dramatically different transitions to test (`fade` vs `radial`) * Ensure scenes are long enough for transition to be visible * Check that browser/player supports transition rendering **Problem:** Transitions do not look smooth, appear jarring. **Solution:** * This is typically expected behavior - transitions have standard duration * Scene content may create visual discord (very different scenes) * Try using `fade` for smoother, more universal transitions * Ensure scenes have sufficient length before/after transition * Consider if scene content naturally flows together * Very short scenes (under 2 seconds) may feel rushed with transitions **Problem:** API returns error about invalid transition value. **Solution:** * Check spelling of transition name exactly as documented * Transition names are case-sensitive (use lowercase) * Valid values: `fade`, `none`, `wipeup`, `wipedown`, `wipeleft`, `wiperight`, `smoothleft`, `smoothright`, `radial`, `circlecrop`, `hblur` * Ensure no extra spaces or special characters * Verify you are passing a string, not an object or number * Example correct usage: `sceneTransition: "fade"` **Problem:** Transition duration seems too fast or too slow. **Solution:** * Transition duration is automatically optimized by the API * You cannot manually control transition duration * All transitions use standard, pre-defined durations * To work around: * Choose different transition types (some feel longer/shorter) * Adjust scene durations to affect overall pacing * Use `none` for instant cuts if transitions feel too slow * This is a current limitation of the transition system **Problem:** Video starts with a transition effect. **Solution:** * First scene's transition applies when moving TO the second scene * There should be no transition at the very start of the video * If you see an effect at video start, it may be: * An intro scene with its own transition * Video player fade-in (not part of your video) * Check that first scene's transition is not being misapplied * The transition at video start should typically be instant (no effect) ## Transition Behavior Examples Understanding exactly when transitions occur: ### Example 1: Three Scenes with Different Transitions ```javascript theme={null} { scenes: [ { story: "Scene A", sceneTransition: "fade" }, // Fade when going A → B { story: "Scene B", sceneTransition: "wiperight" }, // Wipe when going B → C { story: "Scene C", sceneTransition: "none" } // Not used (last scene) ] } ``` **Result:** * Video starts → Scene A (no transition) * Scene A ends → FADE → Scene B begins * Scene B ends → WIPE RIGHT → Scene C begins * Scene C ends → Video ends (no transition) ### Example 2: All Scenes Use Same Transition ```javascript theme={null} { scenes: [ { story: "Scene 1", sceneTransition: "fade" }, { story: "Scene 2", sceneTransition: "fade" }, { story: "Scene 3", sceneTransition: "fade" }, { story: "Scene 4", sceneTransition: "fade" } ] } ``` **Result:** Fade transition between every scene for consistent flow. ### Example 3: No Transitions (Fast Cuts) ```javascript theme={null} { scenes: [ { story: "Scene 1", sceneTransition: "none" }, { story: "Scene 2", sceneTransition: "none" }, { story: "Scene 3", sceneTransition: "none" } ] } ``` **Result:** Instant cuts between all scenes, no transition effects. ## Next Steps Enhance your videos with these complementary features: Add professional intro and outro with transitions Control music per scene with transitions Add music that flows with transitions Style subtitles to complement transitions ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including scene configuration * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # Vimeo Integration Source: https://docs.pictory.ai/guides/advanced-features/vimeo-integration Automatically upload videos to Vimeo with custom privacy settings and folder organization This guide shows you how to create videos with Pictory and automatically upload them to Vimeo with custom privacy settings, content ratings, and folder organization. Perfect for streamlining your video workflow. ## What You'll Learn Automatically upload videos to Vimeo Configure view, embed, and download settings Organize videos in Vimeo folders Set appropriate content ratings ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * A Vimeo account with API access * Vimeo connection created in Pictory (see setup below) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Vimeo Integration Works When you create a video with Vimeo destination: 1. **Video Creation** - Your video is created by Pictory API 2. **Connection Verification** - Vimeo connection is validated 3. **Privacy Setup** - Your specified privacy settings are configured 4. **Folder Assignment** - Video is placed in designated Vimeo folder (if specified) 5. **Upload Process** - Video is automatically uploaded to Vimeo 6. **Response Delivery** - Both Pictory and Vimeo URLs are returned You must create a Vimeo connection before using this feature. The connection authorizes Pictory to upload videos to your Vimeo account on your behalf. ## Setting Up Vimeo Connection Before creating videos with Vimeo integration, set up your connection: ### Step 1: Create Vimeo Connection ```javascript Node.js theme={null} const createConnection = await axios.post( `${API_BASE_URL}/v1/vimeo/connections`, { name: "My Vimeo Account", accessToken: "YOUR_VIMEO_ACCESS_TOKEN" }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const connectionId = createConnection.data.id; console.log("Connection ID:", connectionId); ``` ```python Python theme={null} create_connection = requests.post( f'{API_BASE_URL}/v1/vimeo/connections', json={ 'name': 'My Vimeo Account', 'accessToken': 'YOUR_VIMEO_ACCESS_TOKEN' }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) connection_id = create_connection.json()['id'] print(f"Connection ID: {connection_id}") ``` ### Step 2: List Your Connections ```javascript Node.js theme={null} const connections = await axios.get( `${API_BASE_URL}/v1/vimeo/connections`, { headers: { Authorization: API_KEY } } ); console.log("Your Vimeo connections:", connections.data); ``` ```python Python theme={null} connections = requests.get( f'{API_BASE_URL}/v1/vimeo/connections', headers={'Authorization': API_KEY} ) print("Your Vimeo connections:", connections.json()) ``` See [Create Vimeo Connection](/api-reference/vimeo-integration/create-vimeo-connection) for detailed setup instructions. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media."; async function createVideoWithVimeo() { try { console.log("Creating video with Vimeo upload..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_vimeo", vimeoConnectionId: "{YOUR_VIMEO_CONNECTION_ID}", // Your Vimeo connection ID // Vimeo destination configuration destinations: [ { type: "vimeo", folder_uri: "/videos/123456", // Optional: Vimeo folder URI content_rating: ["safe"], // Content rating // Privacy settings privacy: { view: "unlisted", // Who can view the video embed: "public", // Who can embed the video comments: "anybody", // Who can comment download: false, // Allow downloads add: false, // Allow adding to albums }, }, ], // Scene configuration scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation and Vimeo upload..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video created and uploaded to Vimeo!"); console.log("Pictory URL:", jobResult.data.videoURL); console.log("Vimeo URL:", jobResult.data.destinations?.[0]?.videoLink); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithVimeo(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media." def create_video_with_vimeo(): try: print("Creating video with Vimeo upload...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_vimeo', 'vimeoConnectionId': '{YOUR_VIMEO_CONNECTION_ID}', # Your Vimeo connection ID # Vimeo destination configuration 'destinations': [ { 'type': 'vimeo', 'folder_uri': '/videos/123456', # Optional: Vimeo folder URI 'content_rating': ['safe'], # Content rating # Privacy settings 'privacy': { 'view': 'unlisted', # Who can view the video 'embed': 'public', # Who can embed the video 'comments': 'anybody', # Who can comment 'download': False, # Allow downloads 'add': False # Allow adding to albums } } ], # Scene configuration 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation and Vimeo upload...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video created and uploaded to Vimeo!") print(f"Pictory URL: {job_result['data']['videoURL']}") if job_result['data'].get('destinations'): print(f"Vimeo URL: {job_result['data']['destinations'][0].get('videoLink')}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_vimeo() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ------------------- | ------ | -------- | ---------------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `vimeoConnectionId` | string | Yes | Your Vimeo connection ID from Pictory | | `destinations` | array | Yes | Array of destination configurations (can include multiple) | | `scenes` | array | Yes | Video scene configuration | ### Destination Configuration | Parameter | Type | Required | Description | | ---------------- | ------ | -------- | ----------------------------------------- | | `type` | string | Yes | Must be `"vimeo"` for Vimeo uploads | | `folder_uri` | string | No | Vimeo folder URI (e.g., `/videos/123456`) | | `content_rating` | array | No | Content rating tags (see options below) | | `privacy` | object | No | Privacy and permission settings | ## Privacy Settings Control who can view, embed, and interact with your video: | Setting | Options | Description | | ---------- | --------------------------------------------------------------------------- | -------------------------------------- | | `view` | `anybody`, `contacts`, `disable`, `nobody`, `password`, `unlisted`, `users` | Who can view the video | | `embed` | `private`, `public`, `whitelist` | Who can embed the video on other sites | | `comments` | `anybody`, `contacts`, `nobody` | Who can leave comments | | `download` | `true`, `false` | Allow video downloads | | `add` | `true`, `false` | Allow adding video to albums/channels | ### View Privacy Options Explained | Option | Description | Best Used For | | ---------- | --------------------------------------------------- | ------------------------------------------ | | `anybody` | Public - anyone can view | Public content, marketing videos | | `unlisted` | Anyone with the link can view (not listed publicly) | Shareable but not discoverable content | | `password` | Requires password to view | Confidential presentations, client reviews | | `contacts` | Only your Vimeo contacts can view | Team-only content | | `users` | Only logged-in Vimeo users can view | Restricted professional content | | `nobody` | Private - only you can view | Personal archive, work in progress | | `disable` | Video is disabled | Temporarily unavailable | ### Embed Privacy Options | Option | Description | | ----------- | ------------------------------------------- | | `public` | Anyone can embed the video on their website | | `private` | Video cannot be embedded anywhere | | `whitelist` | Only whitelisted domains can embed | ## Content Rating Options Tag your video with appropriate content ratings: | Rating | Description | | --------------- | --------------------------------- | | `safe` | Safe for general audiences | | `unrated` | No rating specified | | `violence` | Contains violent content | | `drugs` | Contains drug-related content | | `language` | Contains strong language | | `nudity` | Contains nudity or sexual content | | `advertisement` | Commercial or advertising content | You can specify multiple content ratings. For example: `["safe", "advertisement"]` for a family-friendly commercial. ## Common Use Cases ### Public Marketing Video ```javascript theme={null} { vimeoConnectionId: "conn_123abc", destinations: [{ type: "vimeo", content_rating: ["safe", "advertisement"], privacy: { view: "anybody", embed: "public", comments: "anybody", download: false } }] } ``` **Result:** Video is public, embeddable, and allows comments. ### Unlisted Shareable Video ```javascript theme={null} { vimeoConnectionId: "conn_123abc", destinations: [{ type: "vimeo", content_rating: ["safe"], privacy: { view: "unlisted", embed: "public", comments: "contacts", download: false } }] } ``` **Result:** Video accessible via link, embeddable, but not listed publicly. ### Private Client Review ```javascript theme={null} { vimeoConnectionId: "conn_123abc", destinations: [{ type: "vimeo", folder_uri: "/videos/client_reviews", content_rating: ["unrated"], privacy: { view: "password", embed: "private", comments: "contacts", download: true } }] } ``` **Result:** Password-protected video for client review with download option. ### Team-Only Training Video ```javascript theme={null} { vimeoConnectionId: "conn_123abc", destinations: [{ type: "vimeo", folder_uri: "/videos/training", content_rating: ["safe"], privacy: { view: "contacts", embed: "private", comments: "contacts", download: false } }] } ``` **Result:** Private video only viewable by Vimeo contacts. ## Best Practices Protect your Vimeo access credentials: * **Secure Storage:** Store Vimeo access tokens securely, not in code * **Environment Variables:** Use environment variables for sensitive data * **Connection Names:** Use descriptive names for multiple connections * **Regular Rotation:** Periodically rotate access tokens * **Test First:** Verify connection works before production use Select privacy settings based on your use case: * **Public Content:** Use `anybody` view + `public` embed * **Shareable but Private:** Use `unlisted` view * **Team Only:** Use `contacts` or `users` view * **Client Reviews:** Use `password` view with download enabled * **Work in Progress:** Use `nobody` view until ready Use Vimeo folders for better organization: * **Get Folder URI:** Navigate to folder in Vimeo, check URL for URI * **Consistent Structure:** Create logical folder hierarchy * **By Project:** Organize by client, campaign, or project * **By Type:** Separate by content type (marketing, training, etc.) * **Pre-Create Folders:** Set up folders in Vimeo before API use Tag videos appropriately: * **Be Honest:** Accurate ratings help with content discovery * **Multiple Tags:** Use multiple ratings when applicable * **Safe Tag:** Use for family-friendly content * **Advertisement:** Always tag commercial content * **Review Guidelines:** Follow Vimeo's community guidelines You can upload to multiple platforms: * **Multiple Destinations:** Specify up to 5 destinations per video * **Different Settings:** Each destination can have unique settings * **Error Handling:** One destination failure does not stop others * **Track URLs:** Response includes URLs for all destinations * **Verify All:** Check each destination uploaded successfully ## Troubleshooting **Problem:** API returns error about invalid or missing connection. **Solution:** * Verify connection ID is correct * Use Get Vimeo Connections API to list valid connections * Ensure connection hasn't been deleted * Check Vimeo access token is still valid * Re-create connection if necessary * Ensure you are using the ID, not the connection name **Problem:** Video is created successfully but does not appear on Vimeo. **Solution:** * Check job response for destination upload status * Verify Vimeo connection has upload permissions * Check Vimeo account storage quota * Review Vimeo API rate limits * Verify folder URI exists in your Vimeo account * Check destinations array in response for error messages **Problem:** Video uploads but privacy settings are different than specified. **Solution:** * Verify privacy object syntax is correct * Check Vimeo account permissions (some settings require paid plans) * Ensure password is provided if view is set to "password" * Confirm whitelist domains if embed is "whitelist" * Review Vimeo's default settings for your account **Problem:** Video does not appear in specified Vimeo folder. **Solution:** * Get folder URI from Vimeo folder URL (e.g., `/videos/123456`) * Ensure folder exists in your Vimeo account before uploading * Check folder permissions (you must be owner or have access) * Try omitting folder\_uri to upload to account root first * Verify folder URI format starts with `/videos/` **Problem:** Video processing completes but Vimeo upload is slow. **Solution:** * Vimeo upload time depends on video size and length * Large videos (over 1GB) may take 15-30 minutes additional time * Check your internet connection speed * Vimeo may be processing the video on their end * Monitor job status - it will show "completed" when fully done * Consider reducing video resolution/quality if uploads consistently timeout **Problem:** Video created but all destination uploads fail. **Solution:** * Check destinations array is properly formatted * Ensure each destination has required `type` parameter * Verify connection IDs for all destination types * Test with single destination first * Check API response for specific destination errors * Ensure destination limit (5 max) is not exceeded ## Next Steps Explore more advanced features and integrations: Upload videos to AWS S3 storage Add music to your videos Add branded intro and outro sequences Save video projects for later editing ## API Reference For complete technical details on Vimeo integration, see: * [Create Vimeo Connection](/api-reference/vimeo-integration/create-vimeo-connection) - Set up new Vimeo connections * [Get Vimeo Connections](/api-reference/vimeo-integration/get-vimeo-connections) - List all your Vimeo connections * [Get Vimeo Connection by ID](/api-reference/vimeo-integration/get-vimeo-connection-by-id) - Get details of a specific connection * [Update Vimeo Connection](/api-reference/vimeo-integration/update-vimeo-connection) - Modify existing connections * [Delete Vimeo Connection](/api-reference/vimeo-integration/delete-vimeo-connection) - Remove Vimeo connections * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full video creation API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Custom Visual Search Filters Source: https://docs.pictory.ai/guides/advanced-features/visual-search-filters Control visual selection with category and query filters for precise, targeted stock footage matching This guide shows you how to use custom visual search filters to precisely control which stock visuals are selected for each scene. Use category filters and custom search queries to ensure your videos showcase exactly the right imagery for your content. ## What You'll Learn Control visual selection with precision Narrow visuals by specific categories Write effective search queries Use category and query together ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Understanding of your content's visual needs * Familiarity with search query writing ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Visual Search Filters Work When you apply custom visual search filters: 1. **Filter Definition** - You specify category and/or query for each scene 2. **Category Filtering** - System narrows search to specific visual categories 3. **Query Processing** - Custom search query is analyzed for keywords 4. **Visual Search** - Stock library is searched with combined filters 5. **Relevance Ranking** - Results are ranked by relevance to filters 6. **Visual Selection** - Best-matching visuals are selected for the scene 7. **Video Assembly** - Selected visuals are integrated into video scenes Using both `category` and `query` together provides the most precise results. The category narrows the type of content, while the query refines specific characteristics within that category. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithVisualFilters() { try { console.log("Creating video with custom visual search filters..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_with_visual_filters", scenes: [ // SCENE 1: Nature scene with specific landscape { story: "The mountain ranges provide breathtaking views for hikers.", background: { searchFilter: { category: "Nature/Landscapes", // Narrow to landscapes query: "majestic mountain peaks with hiking trails" // Specific query } } }, // SCENE 2: Marine life scene { story: "Marine life thrives in the vibrant coral reefs.", background: { searchFilter: { category: "Animals/Marine_Life", // Narrow to marine animals query: "colorful coral reef with diverse fish species" } } }, // SCENE 3: Technology scene { story: "Modern technology is transforming how we work and communicate.", background: { searchFilter: { category: "Technology/Innovation", // Narrow to tech/innovation query: "advanced technology devices and digital interfaces" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with custom visual filters is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithVisualFilters(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_visual_filters(): try: print("Creating video with custom visual search filters...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_with_visual_filters', 'scenes': [ # SCENE 1: Nature scene with specific landscape { 'story': 'The mountain ranges provide breathtaking views for hikers.', 'background': { 'searchFilter': { 'category': 'Nature/Landscapes', # Narrow to landscapes 'query': 'majestic mountain peaks with hiking trails' # Specific query } } }, # SCENE 2: Marine life scene { 'story': 'Marine life thrives in the vibrant coral reefs.', 'background': { 'searchFilter': { 'category': 'Animals/Marine_Life', # Narrow to marine animals 'query': 'colorful coral reef with diverse fish species' } } }, # SCENE 3: Technology scene { 'story': 'Modern technology is transforming how we work and communicate.', 'background': { 'searchFilter': { 'category': 'Technology/Innovation', # Narrow to tech/innovation 'query': 'advanced technology devices and digital interfaces' } } } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with custom visual filters is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_visual_filters() ``` ## Understanding the Parameters ### Search Filter Configuration | Parameter | Type | Required | Description | | ---------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------- | | `background.searchFilter.category` | string | No | Specific visual category to narrow search results. See available categories below. | | `background.searchFilter.query` | string | No | Custom search query describing desired visuals. More specific queries yield better results. | **Best Practice:** Use both `category` and `query` together for optimal results. Category provides broad filtering, while query adds specific details for precise matching. ## Available Categories ### Nature Categories | Category | Description | Example Use Cases | | ----------------------------- | -------------------------------- | ---------------------------------------------------- | | `Nature/Landscapes` | Mountains, valleys, scenic views | Travel videos, outdoor content, nature documentaries | | `Nature/Plants_and_Trees` | Flora, forests, vegetation | Environmental content, gardening, botanical topics | | `Nature/Sunrises_and_Sunsets` | Dawn and dusk scenes | Inspirational content, time-lapse, scenic b-roll | | `Nature/Weather` | Rain, snow, storms, clouds | Weather reports, climate content, atmospheric scenes | ### Animal Categories | Category | Description | Example Use Cases | | ---------------------- | -------------------------------- | ------------------------------------------------------- | | `Animals/Farm_Animals` | Cows, horses, chickens, sheep | Agriculture, farming, rural life content | | `Animals/Marine_Life` | Fish, coral, ocean creatures | Ocean documentaries, marine biology, underwater content | | `Animals/Pets` | Dogs, cats, domestic animals | Pet care, veterinary content, lifestyle videos | | `Animals/Wildlife` | Wild animals in natural habitats | Wildlife documentaries, conservation, nature shows | ### Business Categories | Category | Description | Example Use Cases | | -------------------------------------------- | ----------------------------- | ------------------------------------------------------ | | `Business_and_Professions/Business_Concepts` | Abstract business visuals | Corporate presentations, business strategy, concepts | | `Business_and_Professions/Office_Work` | Office environments, meetings | Corporate training, workplace content, productivity | | `Business_and_Professions/Professions` | Various professional workers | Career content, professional services, industry videos | ### Technology Categories | Category | Description | Example Use Cases | | ----------------------- | ----------------------------------- | ------------------------------------------------------- | | `Technology/Devices` | Computers, phones, gadgets | Tech reviews, product demos, device tutorials | | `Technology/Innovation` | Cutting-edge tech, digital concepts | Innovation content, future tech, digital transformation | ### Places and Landmarks Categories | Category | Description | Example Use Cases | | ------------------------------------------ | ------------------------------- | ---------------------------------------------------- | | `Places_and_Landmarks/Rural_Areas` | Countryside, villages, farmland | Rural tourism, agricultural content, pastoral scenes | | `Places_and_Landmarks/Tourist_Attractions` | Famous landmarks, monuments | Travel guides, tourism videos, destination content | | `Places_and_Landmarks/Urban_Areas` | Cities, streets, urban life | City guides, urban development, metropolitan content | ### People Categories | Category | Description | Example Use Cases | | ------------------- | ------------------------------------ | ----------------------------------------------------- | | `People/Activities` | People engaged in various activities | Lifestyle content, tutorials, activity demonstrations | | `People/Groups` | Teams, crowds, gatherings | Social content, community videos, team collaboration | | `People/Portraits` | Individual portraits, close-ups | Personal stories, interviews, testimonials | ## Writing Effective Search Queries ### Query Best Practices | Instead of Generic | Use Specific and Descriptive | | ------------------ | ------------------------------------------------------------------------ | | "mountain" | "majestic mountain peaks with snow-capped summits and hiking trails" | | "office" | "modern office space with natural light and collaborative workspace" | | "technology" | "advanced digital interfaces with touchscreens and holographic displays" | | "ocean" | "crystal clear turquoise ocean water with tropical fish and coral" | | "business" | "professional business team meeting in contemporary conference room" | | "city" | "bustling urban cityscape with skyscrapers at golden hour" | ### Query Writing Tips Use detailed descriptions for better visual matching: * **Add Context**: "mountain peaks at sunset" vs just "mountain" * **Include Details**: "colorful coral reef with tropical fish" * **Specify Style**: "modern minimalist office design" * **Add Mood**: "serene lake with morning mist" * **Describe Action**: "entrepreneur working on laptop in cafe" * **Use Adjectives**: "vibrant", "professional", "dynamic", "peaceful" Align search terms with your video's style: * **Professional**: "corporate boardroom", "business professional" * **Casual**: "relaxed outdoor setting", "informal gathering" * **Energetic**: "dynamic motion", "fast-paced action" * **Calm**: "peaceful scenery", "tranquil environment" * **Modern**: "contemporary design", "cutting-edge technology" * **Traditional**: "classic architecture", "timeless aesthetic" Include multiple visual elements in queries: * **Location + Activity**: "people hiking on mountain trail" * **Subject + Environment**: "coral reef in clear tropical waters" * **Object + Context**: "laptop on wooden desk in bright office" * **Action + Setting**: "team collaborating in modern workspace" * **Time + Place**: "sunset over ocean beach" * **Style + Subject**: "minimalist interior design with natural light" Include relevant industry terminology: * **Technology**: "cloud computing", "data visualization", "UI/UX design" * **Healthcare**: "medical equipment", "clinical setting", "patient care" * **Finance**: "stock market", "financial charts", "banking" * **Education**: "classroom learning", "students studying", "academic" * **Real Estate**: "luxury home interior", "architectural design" * **Food**: "gourmet cuisine", "fresh ingredients", "culinary" Keep queries focused and manageable: * **Don't**: "extremely detailed ultra-realistic 4K professional cinematic shot of" * **Do**: "professional high-quality business meeting" * **Don't**: "absolutely perfect mountain vista with every detail" * **Do**: "scenic mountain landscape with clear sky" * **Balance**: Be specific without being unrealistic * **Focus**: 5-10 words is often ideal ## Common Use Cases ### Travel and Tourism Video ```javascript theme={null} { scenes: [ { story: "Explore ancient temples and cultural heritage...", background: { searchFilter: { category: "Places_and_Landmarks/Tourist_Attractions", query: "ancient temple ruins with historical architecture" } } }, { story: "Experience pristine beaches and tropical paradise...", background: { searchFilter: { category: "Nature/Landscapes", query: "pristine white sand beach with palm trees and clear water" } } } ] } ``` **Result:** Travel video with precisely matched destination visuals. ### Corporate Training Video ```javascript theme={null} { scenes: [ { story: "Effective communication drives team success...", background: { searchFilter: { category: "Business_and_Professions/Office_Work", query: "diverse team collaborating in modern office meeting" } } }, { story: "Leadership skills are essential for growth...", background: { searchFilter: { category: "Business_and_Professions/Professions", query: "confident business leader presenting to team" } } } ] } ``` **Result:** Professional training video with appropriate workplace visuals. ### Environmental Documentary ```javascript theme={null} { scenes: [ { story: "Climate change affects our planet's ecosystems...", background: { searchFilter: { category: "Nature/Weather", query: "dramatic storm clouds and changing weather patterns" } } }, { story: "Conservation efforts protect endangered species...", background: { searchFilter: { category: "Animals/Wildlife", query: "endangered wildlife in natural habitat" } } } ] } ``` **Result:** Environmental documentary with relevant nature and wildlife footage. ### Technology Product Launch ```javascript theme={null} { scenes: [ { story: "Introducing our latest innovation in smart devices...", background: { searchFilter: { category: "Technology/Devices", query: "sleek modern smartphone with advanced features" } } }, { story: "Powered by cutting-edge AI technology...", background: { searchFilter: { category: "Technology/Innovation", query: "futuristic digital interface with AI visualization" } } } ] } ``` **Result:** Tech product video with modern, innovative visuals. ## Best Practices Always use both filters together when possible: * **Category Alone**: Too broad, may return irrelevant results * **Query Alone**: Searches across all categories, less targeted * **Both Together**: Narrows to category, then refines with query * **Example**: Category: `Nature/Landscapes` + Query: "mountain sunset" * **Result**: Only landscape visuals matching "mountain sunset" * **Efficiency**: Faster search with more relevant results Choose specific subcategories over general ones: * **Instead of**: Just searching all "Animals" * **Use**: Specific `Animals/Marine_Life` or `Animals/Wildlife` * **Benefit**: More targeted results within category * **Example**: `Business_and_Professions/Office_Work` vs generic business * **Precision**: Subcategories eliminate unrelated visuals * **Quality**: Higher relevance in search results Refine queries based on results: * **Start Broad**: Begin with general query, review results * **Add Details**: Refine query with more specific terms * **Compare**: Test different query phrasings * **Document**: Keep track of queries that work well * **Iterate**: Adjust based on actual visual output * **Build Library**: Create a collection of effective queries for reuse Ensure visual filters align with scene narrative: * **Content Relevance**: Query should match what scene describes * **Tone Consistency**: Visual mood should match narrative tone * **Action Alignment**: If scene describes activity, query should too * **Temporal Matching**: Consider time of day, season in queries * **Emotional Match**: Visuals should support scene emotion * **Avoid Disconnect**: Don't show unrelated visuals to confuse viewers Adapt filters for target audience: * **Local Context**: Use location-specific terms when relevant * **Cultural Sensitivity**: Choose appropriate cultural representations * **Language**: Consider if terms translate well globally * **Regional Specifics**: "downtown Manhattan" vs generic "city" * **Seasonal**: Match seasons to target audience's climate * **Global vs Local**: Balance universal appeal with local relevance ## Troubleshooting **Problem:** Selected visuals do not align with scene content or query. **Solution:** * Make query more specific with additional descriptive terms * Verify category is appropriate for desired content * Try different query phrasing (synonyms, different structure) * Add more context to query (location, time, mood) * Test with simpler query first, then add complexity * Review if category and query are complementary, not contradictory **Problem:** Search returns very generic or seemingly random visuals. **Solution:** * Check that category spelling is exact (case-sensitive) * Verify category exists in available categories list * Try removing category to test if query alone works * Simplify query - may be too specific/complex * Remove uncommon words that might limit results * Use broader category if too specific category has limited stock **Problem:** Setting category does not seem to filter results appropriately. **Solution:** * Verify exact category path: `Nature/Landscapes` (with slash, exact caps) * Check for typos in category name * Ensure category exists in documentation * Try parent category if subcategory too narrow * Test category without query to see category-only results * Review available categories list for correct naming **Problem:** Different scenes get very similar or identical visuals. **Solution:** * Make each scene's query more unique and specific * Add scene-specific details to differentiate queries * Use different categories for different scenes * Include contrasting elements in queries * Add time/location/style differences to queries * Test if stock library has enough variety for your needs **Problem:** Very detailed query returns visuals that do not match well. **Solution:** * Simplify query to core elements * Remove overly specific adjectives or uncommon terms * Focus on 2-3 main visual elements * Test with broader query, then incrementally add detail * Consider if stock library has content matching specific query * Balance specificity with realistic availability **Problem:** Certain terms do not yield expected results. **Solution:** * Use standard English terms (American English typically) * Try synonyms or alternative phrasings * Use industry-standard terminology * Avoid slang, regional terms, or abbreviations * Test common vs technical terminology * Keep language simple and universal ## Filter Strategies ### Strategy 1: Category-First Approach ```javascript theme={null} // Start with category, minimal query { category: "Nature/Landscapes", query: "mountain" } // Then refine query based on results { category: "Nature/Landscapes", query: "snowy mountain peaks at sunrise" } ``` ### Strategy 2: Query-First Approach ```javascript theme={null} // Start with detailed query, no category { query: "modern office team collaboration" } // Then add category for precision { category: "Business_and_Professions/Office_Work", query: "modern office team collaboration" } ``` ### Strategy 3: Balanced Approach (Recommended) ```javascript theme={null} // Use both from start, iterate both { category: "Technology/Innovation", query: "artificial intelligence digital interface" } ``` ## Next Steps Enhance your visual search with these complementary features: Control video background behavior Use custom images and videos Add smooth transitions between visuals Apply consistent branding to visuals ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification including searchFilter configuration * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and get video URLs # AI-Generated Scene Background Images Source: https://docs.pictory.ai/guides/ai-generated-visuals/background-images Generate unique AI images as scene backgrounds using models like Flux, Seedream, Nano Banana, and Nano Banana Pro This guide shows you how to use AI-generated images as scene backgrounds in your videos. Instead of stock visuals, generate custom images from text prompts using a variety of AI image models — each with different quality levels and AI credit costs. ## What You'll Learn Generate unique images with AI instead of stock visuals Choose from Flux, Seedream, Nano Banana, and Nano Banana Pro Apply styles like photorealistic, artistic, cartoon, and more Understand per-image credit costs for each model ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account * Basic understanding of AI image generation concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When you set `background.type` to `"image"` and provide an `aiVisual` configuration, Pictory generates a unique AI image for the scene background: 1. **Prompt Processing** — Your text prompt (or auto-generated prompt from story text) is analyzed 2. **Style Application** — The selected `mediaStyle` is applied to shape the visual output 3. **AI Generation** — The chosen image model creates a unique image 4. **Scene Integration** — The generated image is used as the scene background AI image generation takes additional processing time compared to stock visuals. The time varies by model — faster models like Flux generate in seconds, while higher-quality models take longer. ## Configuration Reference ### Background Object When using AI-generated images, set the `background` object on a scene as follows: | Parameter | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------- | | `type` | string | Yes | Must be `"image"` for AI-generated images | | `aiVisual` | object | Yes | AI image generation configuration (see below) | **Mutually Exclusive:** The `background` object can only have **one** of `visualUrl`, `color`, or `aiVisual`. You cannot combine them in the same scene. ### `aiVisual` Parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | No | Text description of the image to generate (max 500 characters). If omitted, a prompt is auto-generated from the scene's story text. | | `model` | string | Yes | The AI image model to use. See [Available Image Models](#available-image-models). | | `mediaStyle` | string | No | Visual style to apply. See [Available Media Styles](#available-media-styles). | The `videoDuration` parameter is **not allowed** when `type` is `"image"`. It is only used for [AI-generated video clips](/guides/ai-generated-visuals/background-videos). ## Available Image Models Each model has different strengths and AI credit costs. Choose based on your quality requirements and budget. | Model ID | Display Name | AI Credits (per image) | Best For | Supported Aspect Ratios | | ---------------- | --------------- | ---------------------- | ----------------------------- | ----------------------- | | `flux-schnell` | Flux | 0.6 | Reliable for basic layouts | `1:1`, `16:9`, `9:16` | | `seedream3.0` | Seedream | 2 | Reliable for text and numbers | `1:1`, `16:9`, `9:16` | | `nanobanana` | Nano Banana | 4 | Excels at details | `1:1`, `16:9`, `9:16` | | `nanobanana-pro` | Nano Banana Pro | 14 | Superior cinematic quality | `1:1`, `16:9`, `9:16` | **Model Selection Strategy:** * Use `flux-schnell` (0.6 credits) for quick iterations, testing, and drafts * Use `seedream3.0` (2 credits) when your image includes text or numbers * Use `nanobanana` (4 credits) when you need fine detail and precision * Use `nanobanana-pro` (14 credits) for premium, cinematic-quality final output ## Available Media Styles The `mediaStyle` parameter shapes the look and feel of the generated image. It is optional — if omitted, the model uses its default rendering style. | Style | Visual Characteristics | Best Used For | | ---------------- | --------------------------------------- | ----------------------------------------------------------------- | | `photorealistic` | Realistic photographs, natural lighting | Corporate videos, professional presentations, realistic scenarios | | `artistic` | Artistic renderings, painterly effects | Creative content, brand storytelling, abstract concepts | | `cartoon` | Cartoon-style imagery, bold colors | Children's content, educational videos, fun marketing | | `minimalist` | Simple, clean designs, reduced details | Modern branding, tech content, professional minimalism | | `vintage` | Retro aesthetic, aged appearance | Nostalgia marketing, historical content, unique branding | | `futuristic` | Modern, sci-fi look, high-tech feel | Technology content, innovation topics, forward-thinking brands | ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph. The system splits the story into multiple scenes using `createSceneOnEndOfSentence` and auto-generates a visual prompt for each scene from its story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createImagesWithAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-images-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createImagesWithAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_images_with_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-images-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_images_with_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createImagesWithCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-images-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", prompt: "Wide-angle establishing shots of diverse professionals in bright, airy coworking spaces with large windows and natural daylight" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createImagesWithCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_images_with_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-images-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'prompt': 'Wide-angle establishing shots of diverse professionals in bright, airy coworking spaces with large windows and natural daylight' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_images_with_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story and a scene-specific prompt. The prompt directly describes the visual for that scene. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createImagesWithScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-images-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createImagesWithScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_images_with_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-images-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room' } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor' } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'prompt': 'Diverse professionals confidently presenting polished video content on various devices' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_images_with_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createImagesAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-images-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createImagesAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_images_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-images-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic' } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic' } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'image', 'aiVisual': { 'model': 'nanobanana', 'mediaStyle': 'photorealistic' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_images_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 24 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices * **Be specific:** Include details about composition, lighting, and mood * **Use descriptive language:** "bright morning sunlight streaming through glass walls" vs "sunny room" * **Mention key elements:** "modern office with glass walls and city skyline view" * **Keep under 500 characters:** Concise prompts produce more focused results * **Avoid negatives:** Describe what you want, not what you do not want **Good:** "Professional business meeting in modern conference room with natural light and city view" **Poor:** "Not a dark room, people talking, no clutter" | Scenario | Recommended Model | Cost | | -------------------- | ----------------- | ----------- | | Testing & drafts | `flux-schnell` | 0.6 credits | | General content | `seedream3.0` | 2 credits | | Detail-rich scenes | `nanobanana` | 4 credits | | Premium final output | `nanobanana-pro` | 14 credits | Start with `flux-schnell` for iteration, then switch to a higher-quality model for production. * **Corporate/Professional:** `photorealistic` or `minimalist` * **Creative/Artistic:** `artistic` or `vintage` * **Tech/Innovation:** `futuristic` * **Educational/Fun:** `cartoon` * **Consistent branding:** Stick to one style across scenes If your image needs to display readable text or numbers, use `seedream3.0` — it is specifically optimized for rendering text and numbers clearly within generated images. ## Troubleshooting * Make your prompt more specific and detailed * Add descriptive adjectives: "bright", "modern", "spacious" * Specify composition: "aerial view", "close-up", "wide angle" * Include lighting details: "sunset lighting", "studio lighting" * Try a different model — each interprets prompts differently * Switch to a higher-quality model (`nanobanana` or `nanobanana-pro`) * Avoid overly complex prompts — keep them focused * Try a different `mediaStyle` that suits the content Ensure your configuration follows these rules: ```javascript theme={null} // Correct background: { type: "image", aiVisual: { model: "flux-schnell", mediaStyle: "photorealistic" } } // Wrong — missing type background: { aiVisual: { model: "flux-schnell" } } // Wrong — mixing background types background: { type: "image", visualUrl: "https://...", aiVisual: { model: "flux-schnell" } } // Wrong — videoDuration not allowed for images background: { type: "image", aiVisual: { model: "flux-schnell", videoDuration: "5s" } } ``` Each image generation costs AI credits based on the model used. Check your credit balance and consider using a more economical model like `flux-schnell` (0.6 credits per image). ## Next Steps Use AI-generated video clips as scene backgrounds Add professional narration to your videos Apply consistent branding automatically Add music to complement your visuals ## API Reference Direct video rendering with AI visuals Create preview before rendering Monitor storyboard creation progress Search stock visuals as alternative to AI # AI-Generated Scene Background Video Clips Source: https://docs.pictory.ai/guides/ai-generated-visuals/background-videos Generate AI video clips as scene backgrounds using models like Pixverse 5.5, Veo 3.1 Fast, and Veo 3.1 This guide shows you how to use AI-generated video clips as scene backgrounds. Instead of stock footage, generate unique motion video clips from text prompts using AI video models — each offering different quality levels, durations, and AI credit costs. ## What You Will Learn Generate unique video clips as scene backgrounds Choose from Pixverse 5.5, Veo 3.1 Fast, and Veo 3.1 Configure clip duration from 4 to 10 seconds per model Understand per-second credit costs for each model ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account (video generation costs more than image generation) * Basic understanding of AI video generation concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When you set `background.type` to `"video"` and provide an `aiVisual` configuration, Pictory generates a unique AI video clip for the scene background: 1. **Prompt Processing** — Your text prompt (or auto-generated prompt from story text) describes the desired motion video 2. **Video Generation** — The chosen video model creates a unique clip of the specified duration 3. **Scene Integration** — The generated video clip is used as the scene background AI video generation takes significantly longer than image generation and costs more AI credits. The credit cost is calculated **per second of video** generated. Plan for additional processing time and budget accordingly. ## Configuration Reference ### Background Object When using AI-generated video clips, set the `background` object on a scene as follows: | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------- | | `type` | string | Yes | Must be `"video"` for AI-generated video clips | | `aiVisual` | object | Yes | AI video generation configuration (see below) | **Mutually Exclusive:** The `background` object can only have **one** of `visualUrl`, `color`, or `aiVisual`. You cannot combine them in the same scene. ### `aiVisual` Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | No | Text description of the video to generate (max 500 characters). If omitted, a prompt is auto-generated from the scene's story text. | | `model` | string | Yes | The AI video model to use. See [Available Video Models](#available-video-models). | | `videoDuration` | string | No | Duration of the generated clip. Valid values depend on the model. See [Duration Options](#duration-options). If omitted, the model's default (shortest) duration is used. | ## Available Video Models Each model has different quality, supported durations, aspect ratios, and per-second AI credit costs. | Model ID | Display Name | AI Credits (per second) | Best For | | ------------- | ------------ | ----------------------- | --------------------------- | | `pixverse5.5` | Pixverse 5.5 | 1.6 | Reliable for basic motion | | `veo3.1_fast` | Veo 3.1 Fast | 10 | Efficient cinematic quality | | `veo3.1` | Veo 3.1 | 20 | Superior cinematic quality | ### Duration Options Each model supports specific clip durations. If `videoDuration` is omitted or invalid for the selected model, the first (shortest) duration is used as the default. | Model | Available Durations | Default | | ------------- | ----------------------- | ------- | | `pixverse5.5` | `"5s"`, `"8s"`, `"10s"` | `"5s"` | | `veo3.1_fast` | `"4s"`, `"6s"`, `"8s"` | `"4s"` | | `veo3.1` | `"4s"`, `"6s"`, `"8s"` | `"4s"` | ### Supported Aspect Ratios The video aspect ratio is determined by the `aspectRatio` property at the top level of the request. Different models support different aspect ratios: | Model | Supported Aspect Ratios | | ------------- | ----------------------- | | `pixverse5.5` | `16:9`, `9:16`, `1:1` | | `veo3.1_fast` | `16:9`, `9:16` | | `veo3.1` | `16:9`, `9:16` | If the request's `aspectRatio` is not supported by the selected video model, the system automatically falls back to the model's first supported aspect ratio. ### AI Credit Cost Calculator AI credits for video generation are calculated as: **credits per second x duration in seconds**. | Model | 4s | 5s | 6s | 8s | 10s | | ------------- | -- | -- | --- | ---- | --- | | `pixverse5.5` | — | 8 | — | 12.8 | 16 | | `veo3.1_fast` | 40 | — | 60 | 80 | — | | `veo3.1` | 80 | — | 120 | 160 | — | **Cost-Saving Strategy:** * Use `pixverse5.5` at `"5s"` (8 credits) for testing and drafts * Use shorter durations when possible — a 4s clip costs half of an 8s clip * Reserve `veo3.1` for final production where cinematic quality matters ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph. The system splits the story into multiple scenes using `createSceneOnEndOfSentence` and auto-generates a visual prompt for each scene from its story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-video-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createVideoWithAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-video-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_video_with_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-video-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5", prompt: "Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createVideoWithCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-video-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'prompt': 'Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_video_with_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story and a scene-specific prompt. The prompt directly describes the visual for that scene. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-video-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createVideoWithScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-video-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s', 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room' } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s', 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor' } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s', 'prompt': 'Diverse professionals confidently presenting polished video content on various devices' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_video_with_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai-video-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createVideoAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai-video-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s' } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s' } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '5s' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_video_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 48 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices Video prompts should describe **motion and action**, not just a static scene: * **Include movement:** "camera slowly panning across", "particles flowing", "waves crashing" * **Describe transitions:** "sunrise gradually illuminating", "zoom into details" * **Be specific about motion:** "cars driving through city streets" vs. just "city street" * **Keep under 500 characters:** Focused prompts produce better results **Good:** "Aerial drone shot slowly flying over a tropical coastline with turquoise waves rolling onto white sand" **Poor:** "Beach scene" | Scenario | Recommended Model | Duration | Total Cost | | -------------------- | ----------------- | -------- | ------------ | | Testing & drafts | `pixverse5.5` | `"5s"` | 8 credits | | General content | `pixverse5.5` | `"8s"` | 12.8 credits | | Professional quality | `veo3.1_fast` | `"6s"` | 60 credits | | Premium cinematic | `veo3.1` | `"8s"` | 160 credits | Start with `pixverse5.5` for iteration, then switch to `veo3.1_fast` or `veo3.1` for production. Video generation can consume credits quickly. Plan your budget: * Use **shorter durations** when possible — 4s is often enough for a single scene * Use `pixverse5.5` for scenes where basic motion is sufficient * Reserve `veo3.1` for hero scenes that need premium quality * Mix AI video clips with AI images or stock visuals across scenes to manage costs AI video generation takes significantly longer than image generation: * `pixverse5.5`: Fastest video generation * `veo3.1_fast`: Moderate processing time * `veo3.1`: Longest processing time but highest quality * Multiple scenes with AI video clips will multiply total processing time * Consider using AI video for key scenes only Not all models support all aspect ratios: * `pixverse5.5` supports `16:9`, `9:16`, `1:1` * `veo3.1_fast` and `veo3.1` only support `16:9` and `9:16` * If your video uses `1:1` aspect ratio, use `pixverse5.5` * The system auto-corrects unsupported aspect ratios to the model's default ## Troubleshooting * Add motion-specific language to your prompt: "slowly panning", "zooming in", "flowing" * Avoid static descriptions — describe what is happening, not just what is there * Try a higher-quality model for more nuanced motion rendering Each model has specific valid durations: * `pixverse5.5`: `"5s"`, `"8s"`, `"10s"` * `veo3.1_fast`: `"4s"`, `"6s"`, `"8s"` * `veo3.1`: `"4s"`, `"6s"`, `"8s"` Make sure you use a duration supported by your selected model. ```javascript theme={null} // Correct background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s" } } // Wrong — type must be "video" for video models background: { type: "image", aiVisual: { model: "pixverse5.5", videoDuration: "5s" } } // Wrong — missing type background: { aiVisual: { model: "veo3.1", videoDuration: "6s" } } // Wrong — mixing background types background: { type: "video", visualUrl: "https://...", aiVisual: { model: "pixverse5.5" } } ``` Video generation costs AI credits per second. Check your balance: * A `veo3.1` clip at `"8s"` costs **160 credits** per scene * Switch to `pixverse5.5` at `"5s"` (8 credits) for budget-friendly generation * Reduce `videoDuration` to lower the cost If you specify an aspect ratio not supported by the video model, the system automatically falls back to the model's default: * `veo3.1` / `veo3.1_fast` only support `16:9` and `9:16` * Use `pixverse5.5` for `16:9`, `9:16`, and `1:1` aspect ratios ## Next Steps Use AI-generated images as scene backgrounds Add professional narration to your videos Apply consistent branding automatically Add music to complement your visuals ## API Reference Direct video rendering with AI visuals Create preview before rendering Monitor storyboard creation progress Search stock visuals as alternative to AI # First Frame Image Source: https://docs.pictory.ai/guides/ai-generated-visuals/first-frame-image Control the starting frame of AI-generated video clips using firstFrameImageUrl This guide shows you how to use the `firstFrameImageUrl` field to control the starting frame of an AI-generated video clip. By providing an image URL, the AI model generates a video that begins from your image and transitions into the motion described in your prompt. ## What You Will Learn Set a specific image as the starting frame of your AI video Create videos that begin from a known visual state Combine first frame images with text prompts for precise results Understand when and how to use first frame images correctly ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account * A publicly accessible image URL to use as the first frame * Familiarity with [AI-generated background video clips](/guides/ai-generated-visuals/background-videos) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When you provide `firstFrameImageUrl` in the `aiVisual` object: 1. The AI model receives your image as the starting point 2. The model generates a video clip that begins from this image 3. The video transitions from the static image into the motion described by your prompt (or auto-generated prompt from the story text) 4. The result is a video that feels like a natural extension of the provided image `firstFrameImageUrl` is only available when `background.type` is `"video"`. For image generation, use [`referenceImageUrl`](/guides/ai-generated-visuals/reference-image) instead. ## Configuration Add `firstFrameImageUrl` to the `aiVisual` object with a valid image URL: ```json theme={null} { "background": { "type": "video", "aiVisual": { "prompt": "Camera slowly zooms out to reveal a bustling city skyline", "model": "pixverse5.5", "videoDuration": "8s", "firstFrameImageUrl": "https://example.com/city-closeup.jpg" } } } ``` `firstFrameImageUrl` cannot be used together with `referenceImageUrls`. Use one or the other, not both. ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph and a first frame image. The system splits the story into multiple scenes and auto-generates prompts. The first frame image controls the starting visual of the video. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createFirstFrameAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "first-frame-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "8s", firstFrameImageUrl: "https://example.com/starting-frame.jpg" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createFirstFrameAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_first_frame_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'first-frame-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '8s', 'firstFrameImageUrl': 'https://example.com/starting-frame.jpg' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_first_frame_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createFirstFrameCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "first-frame-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "8s", firstFrameImageUrl: "https://example.com/starting-frame.jpg", prompt: "Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createFirstFrameCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_first_frame_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'first-frame-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'videoDuration': '8s', 'firstFrameImageUrl': 'https://example.com/starting-frame.jpg', 'prompt': 'Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_first_frame_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story, a scene-specific prompt, and its own first frame image. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createFirstFrameScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "first-frame-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/creator-at-desk.jpg", prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/ai-workspace.jpg", prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/professionals-presenting.jpg", prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createFirstFrameScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_first_frame_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'first-frame-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/creator-at-desk.jpg', 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room'}} }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/ai-workspace.jpg', 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor'}} }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/professionals-presenting.jpg', 'prompt': 'Diverse professionals confidently presenting polished video content on various devices'}} } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_first_frame_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story and its own first frame image. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createFirstFrameAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "first-frame-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/creator-at-desk.jpg" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/ai-workspace.jpg" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", firstFrameImageUrl: "https://example.com/professionals-presenting.jpg" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createFirstFrameAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_first_frame_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'first-frame-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ {'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/creator-at-desk.jpg'}}}, {'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/ai-workspace.jpg'}}}, {'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'firstFrameImageUrl': 'https://example.com/professionals-presenting.jpg'}}} ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_first_frame_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 48 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices * Use high-quality, well-composed images as your first frame * Ensure the image resolution is appropriate for your video's aspect ratio * Avoid images with heavy text overlays, as AI may distort them during animation * Use images that have natural elements the AI can animate (for example, clouds, water, people) Your prompt should describe the **motion and transition** from the static image, not the image itself: * **Good:** "Camera slowly zooms out revealing the surrounding environment" * **Poor:** "A laptop on a desk" (describes what is already in the image) The AI already has the image as context. Focus your prompt on what should happen next. * The image URL must be publicly accessible (no authentication required) * Use direct image URLs (not page URLs that contain images) * Supported formats include JPEG, PNG, and WebP ## Troubleshooting **Cause:** `firstFrameImageUrl` is used with `type: "image"`. **Resolution:** 1. Change `type` to `"video"` if you want to generate a video clip starting from your image 2. For image generation, use [`referenceImageUrl`](/guides/ai-generated-visuals/reference-image) instead **Cause:** Both `firstFrameImageUrl` and `referenceImageUrls` are provided in the same scene. **Resolution:** 1. Choose one approach: `firstFrameImageUrl` to control the starting frame, or `referenceImageUrls` to guide style 2. Remove the field you do not need **Cause:** The AI model may interpret the image differently depending on the prompt and model used. **Resolution:** 1. Write a prompt that describes the desired motion from the image, not the image itself 2. Try a different model for better image-to-video fidelity 3. Ensure the image URL is accessible and returns a valid image ## Next Steps Create seamless transitions between consecutive scenes Guide AI image generation with a reference image Guide AI video generation with reference images Learn the basics of AI video clip generation ## API Reference Direct video rendering with AI visuals Create preview before rendering # Reference Image for AI Images Source: https://docs.pictory.ai/guides/ai-generated-visuals/reference-image Guide AI image generation with a reference image using referenceImageUrl This guide shows you how to use the `referenceImageUrl` field to guide AI image generation with a reference image. By providing a reference, you can influence the style, composition, and visual tone of the generated image while still using a text prompt for the subject matter. ## What You Will Learn Use a reference image to guide the visual style of AI-generated images Maintain a consistent look across scenes using brand reference images Influence the layout and composition of generated images Understand when and how to use reference images correctly ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account * A publicly accessible image URL to use as a reference * Familiarity with [AI-generated background images](/guides/ai-generated-visuals/background-images) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When you provide `referenceImageUrl` in the `aiVisual` object: 1. The AI model analyzes the reference image for style, color palette, composition, and visual tone 2. Your text prompt describes the subject matter and content to generate 3. The model combines the style influence from the reference image with the content from your prompt 4. The result is an image that matches your prompt's subject matter while adopting visual characteristics from the reference `referenceImageUrl` is only available when `background.type` is `"image"`. For video generation, use [`firstFrameImageUrl`](/guides/ai-generated-visuals/first-frame-image) or [`referenceImageUrls`](/guides/ai-generated-visuals/reference-images-for-video) instead. ## Configuration Add `referenceImageUrl` to the `aiVisual` object with a valid image URL: ```json theme={null} { "background": { "type": "image", "aiVisual": { "prompt": "A modern workspace with natural lighting and indoor plants", "model": "seedream3.0", "mediaStyle": "photorealistic", "referenceImageUrl": "https://example.com/brand-style-reference.jpg" } } } ``` ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph and a reference image to guide style. The system splits the story into multiple scenes and auto-generates prompts. The reference image influences the visual style of all generated images. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImageAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-image-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createReferenceImageAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_image_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-image-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg'}} } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_reference_image_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImageCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-image-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg", prompt: "Wide-angle establishing shots of diverse professionals in bright, airy coworking spaces with large windows and natural daylight" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createReferenceImageCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_image_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-image-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg', 'prompt': 'Wide-angle establishing shots of diverse professionals in bright, airy coworking spaces with large windows and natural daylight'}} } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_reference_image_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story, a scene-specific prompt, and a reference image to guide the visual style. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImageScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-image-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg", prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg", prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg", prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createReferenceImageScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_image_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-image-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ {'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg', 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room'}}}, {'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg', 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor'}}}, {'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg', 'prompt': 'Diverse professionals confidently presenting polished video content on various devices'}}} ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_reference_image_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story and a reference image. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImageAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-image-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "image", aiVisual: { model: "nanobanana", mediaStyle: "photorealistic", referenceImageUrl: "https://example.com/brand-style-reference.jpg" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 10000)); } } createReferenceImageAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_image_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-image-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ {'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg'}}}, {'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg'}}}, {'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'image', 'aiVisual': {'model': 'nanobanana', 'mediaStyle': 'photorealistic', 'referenceImageUrl': 'https://example.com/brand-style-reference.jpg'}}} ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(10) if __name__ == '__main__': create_reference_image_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 24 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices * Use images with clear style characteristics (color palette, lighting, mood) * Avoid overly busy or cluttered reference images * The reference image does not need to match the subject of your prompt; it guides style, not content * High-quality reference images produce better style transfer results You can use `referenceImageUrl` alongside `mediaStyle` for layered control: * `mediaStyle` sets the broad visual category (photorealistic, artistic, cartoon, etc.) * `referenceImageUrl` provides specific style nuances within that category ```json theme={null} { "aiVisual": { "prompt": "Modern office building exterior", "model": "nanobanana", "mediaStyle": "minimalist", "referenceImageUrl": "https://example.com/clean-architecture.jpg" } } ``` * The image URL must be publicly accessible (no authentication required) * Use direct image URLs (not page URLs that contain images) * Supported formats include JPEG, PNG, and WebP ## Troubleshooting **Cause:** `referenceImageUrl` is used with `type: "video"`. **Resolution:** 1. Change `type` to `"image"` if you want to generate AI images 2. For video generation, use [`firstFrameImageUrl`](/guides/ai-generated-visuals/first-frame-image) or [`referenceImageUrls`](/guides/ai-generated-visuals/reference-images-for-video) instead **Cause:** The AI model may weigh the text prompt more heavily than the reference image, or the reference image style characteristics may be subtle. **Resolution:** 1. Use a reference image with strong, distinctive style characteristics 2. Try a higher-quality model like `nanobanana` or `nanobanana-pro` for better style adherence 3. Simplify your text prompt to give the reference image more influence ## Next Steps Create seamless transitions between consecutive scenes Control the starting frame of AI-generated video clips Guide AI video generation with reference images Learn the basics of AI image generation ## API Reference Direct video rendering with AI visuals Create preview before rendering # Reference Images for AI Videos Source: https://docs.pictory.ai/guides/ai-generated-visuals/reference-images-for-video Guide AI video generation with one or two reference images using referenceImageUrls This guide shows you how to use the `referenceImageUrls` field to guide AI video generation with one or two reference images. Unlike [`firstFrameImageUrl`](/guides/ai-generated-visuals/first-frame-image) which sets the starting frame, reference images influence the overall style, composition, and visual tone of the generated video clip. ## What You Will Learn Generate video clips that match the style of your reference images Provide up to two reference images for richer style blending Combine reference images with prompts for precise video generation Understand constraints and model-specific behavior ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account * One or two publicly accessible image URLs to use as references * Familiarity with [AI-generated background video clips](/guides/ai-generated-visuals/background-videos) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When you provide `referenceImageUrls` in the `aiVisual` object: 1. The AI model analyzes the reference images for style, color palette, composition, and visual tone 2. Your text prompt describes the motion and subject matter for the video 3. The model generates a video clip that incorporates the visual characteristics from the reference images 4. With two reference images, the model blends style elements from both for a richer result `referenceImageUrls` is only available when `background.type` is `"video"`. For image generation, use [`referenceImageUrl`](/guides/ai-generated-visuals/reference-image) instead. When using `referenceImageUrls` with the `veo3.1` or `veo3.1_fast` models, the video duration is automatically set to `"8s"` regardless of the `videoDuration` value you provide. ## Configuration Add `referenceImageUrls` to the `aiVisual` object with an array of 1–2 valid image URLs: ```json theme={null} { "background": { "type": "video", "aiVisual": { "prompt": "Dynamic montage of team collaboration in a modern office", "model": "pixverse5.5", "videoDuration": "8s", "referenceImageUrls": [ "https://example.com/office-style-1.jpg", "https://example.com/office-style-2.jpg" ] } } } ``` `referenceImageUrls` cannot be used together with `firstFrameImageUrl`. Use one or the other, not both. ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph and reference images to guide style. The system splits the story into multiple scenes and auto-generates prompts. The reference images influence the visual style of all generated video clips. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImagesAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-images-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "8s", referenceImageUrls: [ "https://example.com/style-reference-1.jpg", "https://example.com/style-reference-2.jpg" ] } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createReferenceImagesAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_images_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-images-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '8s', 'referenceImageUrls': ['https://example.com/style-reference-1.jpg', 'https://example.com/style-reference-2.jpg']}} } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_reference_images_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImagesCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-images-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "8s", referenceImageUrls: ["https://example.com/style-reference-1.jpg", "https://example.com/style-reference-2.jpg"], prompt: "Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createReferenceImagesCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_images_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-images-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '8s', 'referenceImageUrls': ['https://example.com/style-reference-1.jpg', 'https://example.com/style-reference-2.jpg'], 'prompt': 'Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field'}} } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_reference_images_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story, a scene-specific prompt, and reference images. This example shows a mix of one and two reference images across scenes. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImagesScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-images-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/creator-workspace-style.jpg"], prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/tech-workspace-style.jpg", "https://example.com/ai-interface-style.jpg"], prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/professional-presentation-style.jpg"], prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createReferenceImagesScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_images_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-images-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ {'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/creator-workspace-style.jpg'], 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room'}}}, {'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/tech-workspace-style.jpg', 'https://example.com/ai-interface-style.jpg'], 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor'}}}, {'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/professional-presentation-style.jpg'], 'prompt': 'Diverse professionals confidently presenting polished video content on various devices'}}} ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_reference_images_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story and reference images. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createReferenceImagesAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "reference-images-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/creator-workspace-style.jpg"] } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/tech-workspace-style.jpg", "https://example.com/ai-interface-style.jpg"] } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", videoDuration: "5s", referenceImageUrls: ["https://example.com/professional-presentation-style.jpg"] } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get(`${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } }); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createReferenceImagesAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_reference_images_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'reference-images-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': {'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}]}, 'scenes': [ {'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/creator-workspace-style.jpg']}}}, {'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/tech-workspace-style.jpg', 'https://example.com/ai-interface-style.jpg']}}}, {'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': {'type': 'video', 'aiVisual': {'model': 'pixverse5.5', 'videoDuration': '5s', 'referenceImageUrls': ['https://example.com/professional-presentation-style.jpg']}}} ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get(f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True; print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_reference_images_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 48 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices When using two reference images: * Choose images that complement each other rather than conflict * One image can provide color/mood while the other provides composition/structure * Avoid two images with vastly different styles, as the AI may produce inconsistent results Since you are generating video, your prompt should describe motion and action: * **Good:** "Camera slowly panning across a landscape with clouds drifting overhead" * **Poor:** "A beautiful landscape" (describes a static scene) The reference images handle the visual style. Let the prompt focus on what should move and how. When using `veo3.1` or `veo3.1_fast` with reference images, the duration is automatically set to `"8s"`. Plan your scene duration accordingly: * If you need shorter clips, consider using `pixverse5.5` instead * If `"8s"` works for your scene, `veo3.1_fast` provides higher quality with reference images * All image URLs must be publicly accessible (no authentication required) * Use direct image URLs (not page URLs that contain images) * Supported formats include JPEG, PNG, and WebP * The array must contain 1–2 URLs (minimum 1, maximum 2) ## Troubleshooting **Cause:** `referenceImageUrls` is used with `type: "image"`. **Resolution:** 1. Change `type` to `"video"` if you want to generate video clips 2. For image generation, use [`referenceImageUrl`](/guides/ai-generated-visuals/reference-image) (singular) instead **Cause:** Both `firstFrameImageUrl` and `referenceImageUrls` are provided in the same scene. **Resolution:** 1. `firstFrameImageUrl` controls the starting frame; `referenceImageUrls` guides overall style 2. Choose the approach that fits your use case and remove the other field **Cause:** When using `referenceImageUrls` with `veo3.1` or `veo3.1_fast`, the duration is automatically set to `"8s"`. **Resolution:** 1. This is expected behavior and cannot be overridden for these models 2. If you need a different duration, use `pixverse5.5` instead, which supports `"5s"`, `"8s"`, and `"10s"` with reference images **Cause:** The AI model may weigh the text prompt more heavily than the reference images. **Resolution:** 1. Use reference images with strong, distinctive style characteristics 2. Simplify your text prompt to give the reference images more influence 3. Try a different model for better style adherence ## Next Steps Create seamless transitions between consecutive scenes Control the starting frame of AI-generated video clips Guide AI image generation with a reference image Learn the basics of AI video clip generation ## API Reference Direct video rendering with AI visuals Create preview before rendering # Visual Continuity Source: https://docs.pictory.ai/guides/ai-generated-visuals/visual-continuity Create seamless transitions between consecutive AI-generated scenes using visual continuity This guide shows you how to use the `visualContinuity` feature to create smooth, seamless transitions between consecutive AI-generated scenes. When enabled, the system uses the output of each scene as a reference for the next, producing a cohesive visual flow throughout your video. ## What You Will Learn Create seamless transitions between AI-generated video clips Create visually consistent AI-generated image sequences Combine continuity with zoom and pan for cinematic image slideshows Understand the difference continuity makes ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Sufficient AI credits in your account * Familiarity with [AI-generated background images](/guides/ai-generated-visuals/background-images) or [AI-generated background video clips](/guides/ai-generated-visuals/background-videos) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How It Works When `visualContinuity` is set to `true` in the `aiVisual` object: 1. The **first scene** in the sequence is generated normally from its prompt (or auto-generated prompt from the story text) 2. For each **subsequent scene**, the system extracts a reference from the previous scene's output: * **Video scenes:** The last frame of the previous video is used as the first frame for the next scene * **Image scenes:** The generated image of the previous scene is used as a reference for the next scene 3. The AI model blends this reference with the current scene's prompt to produce a visually consistent result ### When Does Continuity Apply? Visual continuity applies in two scenarios: * **Within the same story:** Consecutive scenes that were split from the same original story (for example, when using `createSceneOnEndOfSentence: true`) * **Across consecutive stories:** Consecutive scenes from different stories, enabling seamless transitions across story boundaries In both cases, `visualContinuity` must be set to `true` for the system to use the previous scene's output as a reference. If `visualContinuity` is not set or is `false`, the following behavior applies for consecutive scenes from the same story: * **Image scenes:** All user-provided reference images are cleared, and visuals are generated independently. * **Video scenes:** Only `firstFrameImageUrl` is cleared if it was provided. However, `referenceImageUrls` are preserved and still used for generation. ### Configuration Add `visualContinuity: true` to the `aiVisual` object in your scene's background configuration: ```json theme={null} { "background": { "type": "video", "aiVisual": { "model": "pixverse5.5", "visualContinuity": true } } } ``` *** ## Prompt Behavior with Auto-Created Scenes When you provide a story as a paragraph and use `createSceneOnNewLine: true` and `createSceneOnEndOfSentence: true`, the system automatically splits your story into multiple scenes. The way the `prompt` field behaves depends on whether you provide it or not. ### Without Prompt (Auto-Generated) When you omit the `prompt` field, the system automatically generates a visual prompt for each scene based on that scene's portion of the story text. This is the simplest approach and works well for most use cases. ```json theme={null} { "background": { "type": "video", "aiVisual": { "model": "pixverse5.5", "visualContinuity": true } } } ``` ### With Creative Direction Prompt When you provide a `prompt` alongside a story that will be split into multiple scenes, the prompt acts as a **creative direction** for the entire video. Since scenes are not finalized at the time of the request (the system creates them from the story), the creative direction prompt describes the overall visual style and mood rather than any specific scene. The system uses your creative direction to guide the auto-generated prompts for each individual scene. This ensures a consistent visual tone across all scenes while still generating scene-specific content. A good creative direction prompt follows this structure: **\[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]** **Examples of creative direction prompts:** | Creative Direction Prompt | Use Case | | -------------------------------------------------------------------------------------------------------------------- | ------------------- | | `"Slow cinematic panning shots in modern office environments with warm natural lighting and shallow depth of field"` | Corporate content | | `"Dynamic aerial drone footage over urban landscapes with fast cuts and vibrant neon color grading"` | Marketing video | | `"Gentle zoom transitions in cozy classroom settings with soft pastel tones and hand-drawn illustration style"` | Educational content | | `"Smooth tracking shots through futuristic environments with cool blue lighting and minimalist design"` | Technology content | ```json theme={null} { "background": { "type": "video", "aiVisual": { "model": "pixverse5.5", "visualContinuity": true, "prompt": "Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field" } } } ``` The creative direction prompt applies to the overall video, not to individual scenes. The system combines your creative direction with each scene's story text to generate scene-specific visual prompts that maintain a consistent style throughout the video. *** ## Examples ### Example 1: Single Story Without Prompt One scene with a story paragraph. The system splits the story into multiple scenes using `createSceneOnEndOfSentence` and auto-generates a visual prompt for each scene from its story text. Visual continuity ensures each scene flows naturally into the next. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createContinuityAutoPrompt() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "visual-continuity-auto-prompt", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, sceneTransition: "hblur", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createContinuityAutoPrompt(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_continuity_auto_prompt(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'visual-continuity-auto-prompt', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'sceneTransition': 'hblur', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_continuity_auto_prompt() ``` ### Example 2: Single Story with Creative Direction Same as Example 1, but with a `prompt` that acts as **creative direction** for the entire video. Since the story is split into multiple scenes, the prompt guides the overall visual tone rather than describing a specific scene. A good creative direction prompt follows this structure: \[Action/Movement] + \[Scene/Environment] + \[Camera Technique] + \[Visual Style]. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createContinuityCreativeDirection() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "visual-continuity-creative-direction", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, sceneTransition: "hblur", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true, prompt: "Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createContinuityCreativeDirection(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_continuity_creative_direction(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'visual-continuity-creative-direction', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Every day, millions of content creators, educators, and marketers face the same challenge. They need professional-quality videos but do not have a production team. Hours are spent searching stock libraries for footage that almost fits. Days are lost waiting for design assets. AI-powered video creation changes everything. With a simple text prompt, you can generate custom visuals tailored to your exact narrative. No more settling for generic stock footage. Whether you are building an online course, launching a campaign, or growing your social media presence, AI visuals give you the power to create stunning content in minutes.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True, 'sceneTransition': 'hblur', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True, 'prompt': 'Smooth cinematic tracking shots in modern creative workspaces with warm golden-hour lighting and shallow depth of field' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_continuity_creative_direction() ``` ### Example 3: Multiple Scenes with Prompts Three separate scenes, each with a one-sentence story and a scene-specific prompt. All scenes have `visualContinuity: true` to maintain smooth visual transitions. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createContinuityScenePrompts() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "visual-continuity-scene-prompts", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true, prompt: "A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room" } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true, prompt: "A bright modern workspace with AI-generated visuals appearing on a large monitor" } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true, prompt: "Diverse professionals confidently presenting polished video content on various devices" } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createContinuityScenePrompts(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_continuity_scene_prompts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'visual-continuity-scene-prompts', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True, 'prompt': 'A frustrated creator scrolling through endless stock footage on a laptop in a dimly lit room' } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True, 'prompt': 'A bright modern workspace with AI-generated visuals appearing on a large monitor' } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True, 'prompt': 'Diverse professionals confidently presenting polished video content on various devices' } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_continuity_scene_prompts() ``` ### Example 4: Multiple Scenes Without Prompts Three separate scenes, each with a one-sentence story. No prompts are provided, so the system auto-generates a visual prompt from each scene's story text. Visual continuity connects all scenes. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createContinuityAutoScenes() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "visual-continuity-auto-scenes", language: "en", backgroundMusic: { enabled: true, autoMusic: true, volume: 0.5 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Jackson", speed: 100, amplificationLevel: 0 }] }, scenes: [ { story: "Content creators struggle to find the perfect visuals for their videos.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true } } }, { story: "AI-powered tools generate custom visuals from simple text prompts in minutes.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true } } }, { story: "Professional-quality videos are now accessible to educators and marketers everywhere.", background: { type: "video", aiVisual: { model: "pixverse5.5", visualContinuity: true } } } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Failed: " + JSON.stringify(statusResponse.data)); } await new Promise((resolve) => setTimeout(resolve, 15000)); } } createContinuityAutoScenes(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_continuity_auto_scenes(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'visual-continuity-auto-scenes', 'language': 'en', 'backgroundMusic': {'enabled': True, 'autoMusic': True, 'volume': 0.5}, 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Jackson', 'speed': 100, 'amplificationLevel': 0}] }, 'scenes': [ { 'story': 'Content creators struggle to find the perfect visuals for their videos.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True } } }, { 'story': 'AI-powered tools generate custom visuals from simple text prompts in minutes.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True } } }, { 'story': 'Professional-quality videos are now accessible to educators and marketers everywhere.', 'background': { 'type': 'video', 'aiVisual': { 'model': 'pixverse5.5', 'visualContinuity': True } } } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY}, ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY}, ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception(f"Failed: {status_response.json()}") time.sleep(15) if __name__ == '__main__': create_continuity_auto_scenes() ``` *** ## Tracking AI Credits Used When your video includes AI-generated visuals, the job response includes an `aiCreditsUsed` field that reports the total AI credits consumed across all scenes. This field is present only when at least one scene used `aiVisual` configuration. ```json theme={null} { "job_id": "a1d36612-326d-4b81-aece-411f8aed4c70", "success": true, "data": { "status": "completed", "aiCreditsUsed": 24 } } ``` Use this value to track credit consumption and manage your AI credit budget. For detailed job response documentation, refer to the [Get Storyboard Preview Job](/api-reference/jobs/get-storyboard-preview-job-by-id) or [Get Video Render Job](/api-reference/jobs/get-video-render-job-by-id) API reference. *** ## Best Practices Visual continuity works best when: * Your story follows a consistent narrative or theme * You want a polished, professional look with smooth transitions * Scenes are part of the same visual journey (for example, a product walkthrough or a story arc) Consider turning it off when: * Scenes represent completely different topics or settings * You want each scene to have a distinct, independent visual style * You are mixing different AI models across scenes For the best results, pair `visualContinuity` with scene transitions like `"hblur"`. This creates a double layer of smoothness: * Visual continuity ensures the **content** of consecutive scenes is visually related * Scene transitions ensure the **playback** between scenes is smooth ```json theme={null} { "sceneTransition": "hblur", "background": { "type": "video", "aiVisual": { "model": "pixverse5.5", "visualContinuity": true } } } ``` When using AI-generated images with visual continuity, enable the Ken Burns effect to add subtle zoom and pan animations. This transforms a slideshow of static images into a dynamic, cinematic experience. ```json theme={null} { "background": { "type": "image", "aiVisual": { "model": "nanobanana", "visualContinuity": true, "mediaStyle": "photorealistic" }, "settings": { "kenBurnsEffect": true } } } ``` ## Troubleshooting **Cause:** `visualContinuity` is not set to `true`, or scenes are not consecutive. **Resolution:** 1. Ensure `visualContinuity` is set to `true` in the `aiVisual` object for all scenes that should be connected 2. Verify that scenes are consecutive in the `scenes` array 3. Remember that visual continuity has no effect on the first scene in a sequence **Cause:** The previous scene's output may not have been available as a reference. **Resolution:** 1. When continuity is active and the previous scene's output is unavailable, the system falls back to generating the visual without a reference image 2. Ensure all scenes in the sequence have valid story text or prompts 3. Verify that your AI credit balance is sufficient for all scenes ## Next Steps Control the starting frame of AI-generated video clips Guide AI image generation with a reference image Guide AI video generation with reference images Learn the basics of AI image generation for scene backgrounds ## API Reference Direct video rendering with AI visuals Create preview before rendering # Extend Video with AI Source: https://docs.pictory.ai/guides/ai-studio/extend-video Continue an existing video with AI-generated content using the Pictory AI Studio API This guide shows you how to extend an existing video by generating new content that continues from the end of the original. By providing a video URL and a text prompt, the AI model produces a new video segment that picks up where the original left off. This is useful for building longer narratives, adding follow-up scenes, or expanding short clips. ## What You Will Build Continue an existing video with new AI-generated content Add follow-up scenes to develop a story over multiple segments The AI model maintains visual continuity from the source video Extend videos repeatedly to build longer sequences ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed * A publicly accessible URL of the video you want to extend ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials, the source video URL, and a prompt that describes what should happen next in the video. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Video extension configuration const videoRequest = { prompt: "The woman sits down on the grass as children run and play around her in a wide shot", extendVideoUrl: "https://example.com/videos/woman-walking-in-park.mp4", model: "pixverse5.5", aspectRatio: "9:16", duration: "8s" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Video extension configuration video_request = { "prompt": "The woman sits down on the grass as children run and play around her in a wide shot", "extendVideoUrl": "https://example.com/videos/woman-walking-in-park.mp4", "model": "pixverse5.5", "aspectRatio": "9:16", "duration": "8s" } ``` The `extendVideoUrl` must point to a publicly accessible video file. This parameter cannot be used together with `firstFrameImageUrl` or `referenceImageUrls`. ### Step 2: Submit the Video Extension Request Send the request to the AI Studio video generation endpoint. The API processes the source video and generates a new segment that continues from it. ```javascript Node.js theme={null} async function extendVideo() { try { console.log("Submitting video extension request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/videos`, videoRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video extension started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def extend_video(): try: print("Submitting video extension request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/videos", json=video_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Video extension started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the extended video is ready. ```javascript Node.js theme={null} async function waitForVideo(jobId) { console.log("\nPolling for video extension result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nVideo extended successfully!"); console.log("Video URL:", data.data.url); console.log("Duration:", data.data.duration); console.log("Dimensions:", data.data.width, "x", data.data.height); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Video extension failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow extendVideo() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_video(job_id): print("\nPolling for video extension result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nVideo extended successfully!") print(f"Video URL: {data['data']['url']}") print(f"Duration: {data['data']['duration']}") print(f"Dimensions: {data['data']['width']} x {data['data']['height']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Video extension failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = extend_video() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | ---------------- | ------ | -------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A text description of what should happen next in the video. Must be between 5 and 5,000 characters. | | `extendVideoUrl` | string | No | — | A publicly accessible URL of the video to extend. Must be a valid URI. Cannot be used together with `firstFrameImageUrl`. | | `model` | string | No | `pixverse5.5` | The AI model to use for generation. Supported values: `veo3.1`, `veo3.1_fast`, `pixverse5.5`. See [Generate Video API](/api-reference/ai-studio/generate-video) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the model. For example, `pixverse5.5` supports `16:9`, `9:16`, `1:1`, `3:4`, `4:3`, while `veo3.1` supports `16:9`, `9:16`. | | `duration` | string | No | First supported duration of the selected model | The duration of the extended segment. Valid values depend on the model. For example, `pixverse5.5` supports `5s`, `8s`, `10s`, while `veo3.1` supports `4s`, `6s`, `8s`. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Building Multi-Segment Videos You can extend videos iteratively to build longer sequences. Each extension generates a new video segment that continues from the previous one. **Example workflow:** 1. Generate the first video segment using a text prompt. 2. Retrieve the video URL from the completed job. 3. Use that URL as the `extendVideoUrl` in a new request with a prompt for the next scene. 4. Repeat to build a multi-segment video. Each extension request generates a standalone video file, not an appended version of the original. To combine segments into a final video, you will need to concatenate them using a video editing tool or library after all segments are generated. ## Tips for Extending Videos * **Describe the transition naturally.** Write your prompt as a continuation of the action. For example, if the source video shows someone walking, your prompt might say "She stops and turns to look at the sunset." * **Maintain consistency.** Use the same `model` and `aspectRatio` as the original video for the best visual continuity between segments. * **Keep prompts grounded.** Reference what is happening at the end of the source video. Abrupt changes in subject or setting may produce less coherent results. * **Plan your narrative.** When building multi-segment sequences, outline the full story before generating. This helps you write prompts that flow naturally from one segment to the next. ## Next Steps * [Generate Video from Text Prompt](/guides/ai-studio/text-to-video) to create videos from scratch * [Generate Video from First Frame](/guides/ai-studio/video-from-first-frame) to animate from a specific starting image * [Generate Video from Reference Images](/guides/ai-studio/video-from-reference-images) to guide generation with reference visuals * [Generate Video API Reference](/api-reference/ai-studio/generate-video) for the complete parameter documentation # Generate Image from Reference Image Source: https://docs.pictory.ai/guides/ai-studio/image-from-reference Create AI-generated images guided by a reference image using the Pictory AI Studio API This guide shows you how to generate an AI image using both a text prompt and a reference image. The reference image provides visual context that guides the AI model, allowing you to create variations, apply transformations, or produce new compositions inspired by the original. ## What You Will Build Use an existing image to guide the AI output Generate new images that draw from a source composition Replace or modify subjects while preserving scene context Apply different visual styles to referenced content ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed * A publicly accessible URL for your reference image ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials, the reference image URL, and the prompt that describes the desired transformation or output. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Image generation with reference image const imageRequest = { prompt: "Replace the dog with a white cat sitting in the same position on the park bench in the referenced image", model: "seedream3.0", aspectRatio: "16:9", style: "photorealistic", referenceImageUrl: "https://example.com/images/park-bench-scene.png" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Image generation with reference image image_request = { "prompt": "Replace the dog with a white cat sitting in the same position on the park bench in the referenced image", "model": "seedream3.0", "aspectRatio": "16:9", "style": "photorealistic", "referenceImageUrl": "https://example.com/images/park-bench-scene.png" } ``` The `referenceImageUrl` must point to a publicly accessible image. Ensure the URL does not require authentication or session cookies to access. ### Step 2: Submit the Image Generation Request Send the request to the AI Studio image generation endpoint. The process is identical to text-to-image generation, with the addition of the `referenceImageUrl` field. ```javascript Node.js theme={null} async function generateImageFromReference() { try { console.log("Submitting reference-based image generation request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/images`, imageRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Image generation started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def generate_image_from_reference(): try: print("Submitting reference-based image generation request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/images", json=image_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Image generation started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the image is ready. ```javascript Node.js theme={null} async function waitForImage(jobId) { console.log("\nPolling for image generation result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nImage generated successfully!"); console.log("Image URL:", data.data.url); console.log("Dimensions:", data.data.width, "x", data.data.height); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Image generation failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow generateImageFromReference() .then(jobId => waitForImage(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_image(job_id): print("\nPolling for image generation result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nImage generated successfully!") print(f"Image URL: {data['data']['url']}") print(f"Dimensions: {data['data']['width']} x {data['data']['height']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Image generation failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = generate_image_from_reference() result = wait_for_image(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | ------------------- | ------ | -------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A text description that explains what to generate, referencing or modifying the source image. Must be between 5 and 5,000 characters. | | `model` | string | No | `seedream3.0` | The AI model to use for generation. Supported values: `seedream3.0`, `flux-schnell`, `nanobanana`, `nanobanana-pro`. See [Generate Image API](/api-reference/ai-studio/generate-image) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the selected model (e.g., `1:1`, `16:9`, `9:16`). | | `style` | string | No | — | Visual style to apply to the generated image. Supported values: `photorealistic`, `artistic`, `cartoon`, `minimalist`, `vintage`, `futuristic`. | | `referenceImageUrl` | string | No | — | A publicly accessible URL of the image to use as a visual reference. Must be a valid URI. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Tips for Reference-Based Generation * **Describe the change clearly.** State what should be modified from the reference image. For example, "Replace the car with a bicycle" is more effective than "Change the vehicle." * **Mention the reference explicitly.** Use phrases such as "in the referenced image" or "based on the provided image" so the model understands the relationship between the prompt and the reference. * **Preserve scene context.** If you want to keep the background or composition, say so in the prompt. For example, "Keep the background and lighting unchanged." * **Combine with styles.** Applying a `style` such as `"artistic"` or `"cartoon"` transforms the reference into a different visual treatment while preserving the subject and layout. ## Next Steps * [Generate Image from Text Prompt](/guides/ai-studio/text-to-image) to create images without a reference * [Generate Image API Reference](/api-reference/ai-studio/generate-image) for the complete parameter documentation # Generate Image from Text Prompt Source: https://docs.pictory.ai/guides/ai-studio/text-to-image Create AI-generated images from descriptive text prompts using the Pictory AI Studio API This guide walks you through generating an AI image from a text prompt using the Pictory AI Studio API. You will learn how to submit a prompt, select a model and style, and retrieve the generated image by polling the job status. ## What You Will Build Generate images from descriptive text prompts Choose from multiple AI image models Apply visual styles such as photorealistic, artistic, or cartoon Monitor image generation and retrieve the result ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials and define the image prompt along with the desired model and style. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Image generation configuration const imageRequest = { prompt: "A golden retriever running through a sunlit meadow with wildflowers in the background", model: "seedream3.0", aspectRatio: "16:9", style: "photorealistic" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Image generation configuration image_request = { "prompt": "A golden retriever running through a sunlit meadow with wildflowers in the background", "model": "seedream3.0", "aspectRatio": "16:9", "style": "photorealistic" } ``` ### Step 2: Submit the Image Generation Request Send the request to the AI Studio image generation endpoint. On success, the API returns a `jobId` that you will use to poll for the result. ```javascript Node.js theme={null} async function generateImage() { try { console.log("Submitting image generation request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/images`, imageRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Image generation started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def generate_image(): try: print("Submitting image generation request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/images", json=image_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Image generation started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the image is ready. The recommended polling interval is 10 to 30 seconds. ```javascript Node.js theme={null} async function waitForImage(jobId) { console.log("\nPolling for image generation result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nImage generated successfully!"); console.log("Image URL:", data.data.url); console.log("Dimensions:", data.data.width, "x", data.data.height); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Image generation failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow generateImage() .then(jobId => waitForImage(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_image(job_id): print("\nPolling for image generation result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nImage generated successfully!") print(f"Image URL: {data['data']['url']}") print(f"Dimensions: {data['data']['width']} x {data['data']['height']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Image generation failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = generate_image() result = wait_for_image(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | ------------------- | ------ | -------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A descriptive text of the image to generate. Must be between 5 and 5,000 characters. | | `model` | string | No | `seedream3.0` | The AI model to use for generation. Supported values: `seedream3.0`, `flux-schnell`, `nanobanana`, `nanobanana-pro`. See [Generate Image API](/api-reference/ai-studio/generate-image) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the selected model (e.g., `1:1`, `16:9`, `9:16`). | | `style` | string | No | — | Visual style to apply to the generated image. Supported values: `photorealistic`, `artistic`, `cartoon`, `minimalist`, `vintage`, `futuristic`. | | `referenceImageUrl` | string | No | — | A publicly accessible URL of a reference image to guide the generation. Must be a valid URI. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Tips for Effective Prompts * **Be specific and descriptive.** Include details about the subject, setting, lighting, and composition. * **Specify the camera angle.** Use terms such as "wide shot", "close-up", or "aerial view" to control framing. * **Pair with a style.** Combine your prompt with the `style` parameter for more consistent results. For example, use `"photorealistic"` for lifelike images or `"artistic"` for a painterly look. * **Keep prompts focused.** A single clear subject with a well-defined setting produces better results than a prompt that tries to describe too many elements at once. ## Next Steps * [Generate Image from Reference Image](/guides/ai-studio/image-from-reference) to use an existing image as a visual guide * [Generate Image API Reference](/api-reference/ai-studio/generate-image) for the complete parameter documentation # Generate Video from Text Prompt Source: https://docs.pictory.ai/guides/ai-studio/text-to-video Create AI-generated videos from descriptive text prompts using the Pictory AI Studio API This guide walks you through generating an AI video from a text prompt using the Pictory AI Studio API. You will learn how to submit a prompt, select a model, configure the aspect ratio and duration, and retrieve the generated video by polling the job status. ## What You Will Build Generate videos from descriptive text prompts Choose from multiple AI video models Set the video length based on your needs Generate landscape, portrait, or square videos ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials and define the video prompt along with the desired model, aspect ratio, and duration. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Video generation configuration const videoRequest = { prompt: "A young woman sitting on a couch in a modern living room, watching television with soft ambient lighting in a wide camera shot", model: "pixverse5.5", aspectRatio: "9:16", duration: "8s" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Video generation configuration video_request = { "prompt": "A young woman sitting on a couch in a modern living room, watching television with soft ambient lighting in a wide camera shot", "model": "pixverse5.5", "aspectRatio": "9:16", "duration": "8s" } ``` ### Step 2: Submit the Video Generation Request Send the request to the AI Studio video generation endpoint. On success, the API returns a `jobId` that you will use to poll for the result. ```javascript Node.js theme={null} async function generateVideo() { try { console.log("Submitting video generation request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/videos`, videoRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video generation started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def generate_video(): try: print("Submitting video generation request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/videos", json=video_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Video generation started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the video is ready. The recommended polling interval is 10 to 30 seconds. Video generation typically takes longer than image generation. ```javascript Node.js theme={null} async function waitForVideo(jobId) { console.log("\nPolling for video generation result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nVideo generated successfully!"); console.log("Video URL:", data.data.url); console.log("Duration:", data.data.duration); console.log("Dimensions:", data.data.width, "x", data.data.height); console.log("Thumbnail:", data.data.thumbnailImageUrl); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Video generation failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow generateVideo() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_video(job_id): print("\nPolling for video generation result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nVideo generated successfully!") print(f"Video URL: {data['data']['url']}") print(f"Duration: {data['data']['duration']}") print(f"Dimensions: {data['data']['width']} x {data['data']['height']}") print(f"Thumbnail: {data['data']['thumbnailImageUrl']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Video generation failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = generate_video() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | ------------- | ------ | -------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A descriptive text of the video to generate. Must be between 5 and 5,000 characters. | | `model` | string | No | `pixverse5.5` | The AI model to use for generation. Supported values: `veo3.1`, `veo3.1_fast`, `pixverse5.5`. See [Generate Video API](/api-reference/ai-studio/generate-video) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the model. For example, `pixverse5.5` supports `16:9`, `9:16`, `1:1`, `3:4`, `4:3`, while `veo3.1` supports `16:9`, `9:16`. | | `duration` | string | No | First supported duration of the selected model | The video length. Valid values depend on the model. For example, `pixverse5.5` supports `5s`, `8s`, `10s`, while `veo3.1` supports `4s`, `6s`, `8s`. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Tips for Effective Video Prompts * **Describe motion and action.** Unlike image prompts, video prompts benefit from describing movement. For example, "A bird taking flight from a tree branch" is more effective than "A bird on a tree." * **Specify the camera angle.** Terms such as "wide shot", "close-up", "tracking shot", or "aerial view" help the model determine the framing and camera movement. * **Keep the scene focused.** A single clear action with one or two subjects produces better results than a prompt describing multiple simultaneous events. * **Consider the duration.** Shorter durations (4 to 5 seconds) work well for simple motions, while longer durations (8 to 10 seconds) allow for more complex scenes. ## Next Steps * [Generate Video from First Frame](/guides/ai-studio/video-from-first-frame) to start a video from a specific image * [Generate Video from Reference Images](/guides/ai-studio/video-from-reference-images) to guide video generation using reference visuals * [Extend Video with AI](/guides/ai-studio/extend-video) to continue an existing video with new content * [Generate Video API Reference](/api-reference/ai-studio/generate-video) for the complete parameter documentation # Generate Video from First Frame Source: https://docs.pictory.ai/guides/ai-studio/video-from-first-frame Create AI-generated videos starting from a specific image as the opening frame This guide shows you how to generate an AI video that begins from a specific image. By providing a first frame image along with a text prompt, you can control the visual starting point of the video while letting the AI model animate the scene based on your description. ## What You Will Build Set a specific image as the opening frame of your video Describe how the scene should evolve from the starting frame Chain videos together using the last frame of a previous generation Maintain visual consistency by starting from a known frame ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed * A publicly accessible URL for your first frame image ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` You can use the `lastFrameImageUrl` from a completed video generation job as the `firstFrameImageUrl` for the next request. This technique allows you to create seamless multi-segment videos. ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials, the first frame image URL, and a prompt that describes how the video should proceed from that frame. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Video generation from first frame const videoRequest = { prompt: "The woman in the frame stands up and walks towards the large window overlooking the city skyline in a wide shot", firstFrameImageUrl: "https://example.com/images/woman-seated-living-room.png", model: "pixverse5.5", aspectRatio: "9:16", duration: "8s" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Video generation from first frame video_request = { "prompt": "The woman in the frame stands up and walks towards the large window overlooking the city skyline in a wide shot", "firstFrameImageUrl": "https://example.com/images/woman-seated-living-room.png", "model": "pixverse5.5", "aspectRatio": "9:16", "duration": "8s" } ``` The `firstFrameImageUrl` must point to a publicly accessible image. This parameter cannot be used together with `extendVideoUrl` or `referenceImageUrls`. ### Step 2: Submit the Video Generation Request Send the request to the AI Studio video generation endpoint. ```javascript Node.js theme={null} async function generateVideoFromFrame() { try { console.log("Submitting first-frame video generation request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/videos`, videoRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video generation started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def generate_video_from_frame(): try: print("Submitting first-frame video generation request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/videos", json=video_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Video generation started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the video is ready. ```javascript Node.js theme={null} async function waitForVideo(jobId) { console.log("\nPolling for video generation result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nVideo generated successfully!"); console.log("Video URL:", data.data.url); console.log("Duration:", data.data.duration); console.log("Last Frame URL:", data.data.lastFrameImageUrl); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Video generation failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow generateVideoFromFrame() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_video(job_id): print("\nPolling for video generation result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nVideo generated successfully!") print(f"Video URL: {data['data']['url']}") print(f"Duration: {data['data']['duration']}") print(f"Last Frame URL: {data['data']['lastFrameImageUrl']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Video generation failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = generate_video_from_frame() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | -------------------- | ------ | -------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A text description of how the video should animate from the first frame. Must be between 5 and 5,000 characters. | | `firstFrameImageUrl` | string | No | — | A publicly accessible URL of the image to use as the opening frame. Must be a valid URI. Cannot be used together with `extendVideoUrl` or `referenceImageUrls`. | | `model` | string | No | `pixverse5.5` | The AI model to use for generation. Supported values: `veo3.1`, `veo3.1_fast`, `pixverse5.5`. See [Generate Video API](/api-reference/ai-studio/generate-video) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the model. For example, `pixverse5.5` supports `16:9`, `9:16`, `1:1`, `3:4`, `4:3`, while `veo3.1` supports `16:9`, `9:16`. | | `duration` | string | No | First supported duration of the selected model | The video length. Valid values depend on the model. For example, `pixverse5.5` supports `5s`, `8s`, `10s`, while `veo3.1` supports `4s`, `6s`, `8s`. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Chaining Videos with Last Frame The completed video response includes a `lastFrameImageUrl` field. You can use this URL as the `firstFrameImageUrl` in a new request to create a seamless continuation. This technique is useful for building longer narratives across multiple video segments. **Example workflow:** 1. Generate the first video with a text prompt. 2. Retrieve the `lastFrameImageUrl` from the completed job. 3. Use that URL as the `firstFrameImageUrl` for the next video, with a new prompt describing the next action. 4. Repeat to build a multi-segment video sequence. ## Tips for First Frame Videos * **Reference the frame content.** Describe actions relative to what is visible in the image. For example, "The person in the frame begins to walk" is more effective than a prompt that ignores the image content. * **Describe motion direction.** Specify where subjects should move. For example, "walks towards the window" or "turns to face the camera." * **Match the aspect ratio.** Use the same aspect ratio as your source image for the best visual continuity. * **Use for scene transitions.** Generate an image with the AI Studio image endpoint, then animate it with this approach for full creative control over the opening frame. ## Next Steps * [Generate Video from Text Prompt](/guides/ai-studio/text-to-video) to create videos without a starting frame * [Generate Video from Reference Images](/guides/ai-studio/video-from-reference-images) to guide generation using multiple reference visuals * [Extend Video with AI](/guides/ai-studio/extend-video) to continue an existing video * [Generate Video API Reference](/api-reference/ai-studio/generate-video) for the complete parameter documentation # Generate Video from Reference Images Source: https://docs.pictory.ai/guides/ai-studio/video-from-reference-images Create AI-generated videos guided by one or more reference images using the Pictory AI Studio API This guide shows you how to generate an AI video using reference images along with a text prompt. By providing up to three reference images, you can influence the subjects, style, and composition of the generated video. This is particularly useful for combining elements from multiple images into a single video scene. ## What You Will Build Use up to three images to guide the video output Combine subjects from different images into one scene Maintain character or scene appearance across generations Steer the video style and content with visual references ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed * One to three publicly accessible image URLs to use as references ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request Prepare your API credentials, the reference image URLs, and a prompt that describes the desired video. The prompt should explain how the subjects or elements from the reference images should appear in the video. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Video generation with reference images const videoRequest = { prompt: "The woman from the first image walks through the open field from the second image, approaching the farmhouse in the distance in a wide shot", referenceImageUrls: [ "https://example.com/images/woman-portrait-outdoor.png", "https://example.com/images/green-field-farmhouse.png" ], model: "pixverse5.5", aspectRatio: "9:16", duration: "8s" }; ``` ```python Python theme={null} import requests import time API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" # Replace with your actual API key # Video generation with reference images video_request = { "prompt": "The woman from the first image walks through the open field from the second image, approaching the farmhouse in the distance in a wide shot", "referenceImageUrls": [ "https://example.com/images/woman-portrait-outdoor.png", "https://example.com/images/green-field-farmhouse.png" ], "model": "pixverse5.5", "aspectRatio": "9:16", "duration": "8s" } ``` The `referenceImageUrls` array accepts 1 to 3 image URLs. All URLs must point to publicly accessible images. This parameter cannot be used together with `firstFrameImageUrl` or `extendVideoUrl`. ### Step 2: Submit the Video Generation Request Send the request to the AI Studio video generation endpoint. ```javascript Node.js theme={null} async function generateVideoFromReferences() { try { console.log("Submitting reference-based video generation request..."); const response = await axios.post( `${API_BASE_URL}/v1/aistudio/videos`, videoRequest, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video generation started."); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error submitting request:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def generate_video_from_references(): try: print("Submitting reference-based video generation request...") response = requests.post( f"{API_BASE_URL}/v1/aistudio/videos", json=video_request, headers={ "Content-Type": "application/json", "Authorization": API_KEY } ) response.raise_for_status() job_id = response.json()["data"]["jobId"] print("Video generation started.") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error submitting request: {error}") raise ``` ### Step 3: Poll for the Result Check the job status at regular intervals until the video is ready. ```javascript Node.js theme={null} async function waitForVideo(jobId) { console.log("\nPolling for video generation result..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const data = response.data; const status = data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nVideo generated successfully!"); console.log("Video URL:", data.data.url); console.log("Duration:", data.data.duration); console.log("Dimensions:", data.data.width, "x", data.data.height); console.log("AI Credits Used:", data.data.aiCreditsUsed); return data; } if (status === "failed") { throw new Error("Video generation failed: " + JSON.stringify(data)); } // Wait 15 seconds before polling again await new Promise(resolve => setTimeout(resolve, 15000)); } } // Run the complete workflow generateVideoFromReferences() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_video(job_id): print("\nPolling for video generation result...") while True: response = requests.get( f"{API_BASE_URL}/v1/jobs/{job_id}", headers={"Authorization": API_KEY} ) response.raise_for_status() data = response.json() status = data["data"]["status"] print(f"Status: {status}") if status == "completed": print("\nVideo generated successfully!") print(f"Video URL: {data['data']['url']}") print(f"Duration: {data['data']['duration']}") print(f"Dimensions: {data['data']['width']} x {data['data']['height']}") print(f"AI Credits Used: {data['data']['aiCreditsUsed']}") return data if status == "failed": raise Exception(f"Video generation failed: {data}") # Wait 15 seconds before polling again time.sleep(15) # Run the complete workflow if __name__ == "__main__": job_id = generate_video_from_references() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the Parameters | Parameter | Type | Required | Default | Description | | -------------------- | ---------------- | -------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `prompt` | string | Yes | — | A text description that explains how the reference images should be used in the video. Must be between 5 and 5,000 characters. | | `referenceImageUrls` | array of strings | No | — | An array of 1 to 3 publicly accessible image URLs to guide the generation. Each entry must be a valid URI. Cannot be used together with `firstFrameImageUrl` or `extendVideoUrl`. | | `model` | string | No | `pixverse5.5` | The AI model to use for generation. Supported values: `veo3.1`, `veo3.1_fast`, `pixverse5.5`. See [Generate Video API](/api-reference/ai-studio/generate-video) for model capabilities and pricing. | | `aspectRatio` | string | No | First supported ratio of the selected model | The output aspect ratio. Valid values depend on the model. For example, `pixverse5.5` supports `16:9`, `9:16`, `1:1`, `3:4`, `4:3`, while `veo3.1` supports `16:9`, `9:16`. | | `duration` | string | No | First supported duration of the selected model | The video length. Valid values depend on the model. For example, `pixverse5.5` supports `5s`, `8s`, `10s`, while `veo3.1` supports `4s`, `6s`, `8s`. | | `webhook` | string | No | — | A URL to receive a POST notification when the job completes. Must be a valid URI. | ## Use Cases for Reference Images | Use Case | Number of Images | Description | | ------------------------- | ---------------- | ----------------------------------------------------------------------------- | | **Character consistency** | 1 | Provide a character portrait to maintain appearance across video generations | | **Scene composition** | 2 | Combine a character image with a background or environment image | | **Multi-subject scenes** | 2 to 3 | Provide images of different subjects that should appear together in the video | | **Style reference** | 1 | Use an image with the desired visual style to influence the output | ## Tips for Reference Image Videos * **Describe the relationship between images.** In your prompt, explain how the elements from each reference should interact. For example, "The person from the first image walks into the landscape from the second image." * **Use high-quality references.** Clear, well-lit images produce better results. Avoid blurry or heavily compressed images. * **Keep subjects distinct.** When combining multiple images, ensure each reference contributes a clearly identifiable element such as a character, background, or object. * **Order matters.** Reference the images by position ("the first image", "the second image") in your prompt so the model can correctly associate each image with the corresponding subject. ## Next Steps * [Generate Video from Text Prompt](/guides/ai-studio/text-to-video) to create videos without reference images * [Generate Video from First Frame](/guides/ai-studio/video-from-first-frame) to animate from a specific starting frame * [Extend Video with AI](/guides/ai-studio/extend-video) to continue an existing video * [Generate Video API Reference](/api-reference/ai-studio/generate-video) for the complete parameter documentation # Blog URL to Video Source: https://docs.pictory.ai/guides/article-to-video/blog-to-video Convert any blog article or webpage into a video by providing its URL This guide shows you how to convert blog articles and web pages into engaging videos. Simply provide a URL and the API automatically extracts content, selects visuals, and creates a complete video - perfect for repurposing written content for social media and video platforms. ## What You'll Learn Convert blog URLs to video storyboards automatically Automatic content extraction and processing AI-selected visuals matched to your content Track video creation progress in real-time ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * A publicly accessible blog article or webpage URL * Basic understanding of web content structure ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Blog-to-Video Works When you convert a blog URL to video: 1. **URL Access** - The API accesses your provided URL 2. **Content Extraction** - Text content is scraped from the web page 3. **Content Analysis** - The content is analyzed for key topics and themes 4. **Scene Generation** - Content is intelligently split into logical video scenes 5. **Visual Selection** - Appropriate stock visuals are matched to each scene 6. **Caption Generation** - Subtitles are created from the extracted text 7. **Video Rendering** - The final video is assembled and rendered The URL must be publicly accessible without requiring login, subscription, or paywall access. The API uses intelligent parsing to extract the main article content while filtering out ads and sidebars. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample blog URL - replace with your own blog URL const BLOG_URL = "https://blog.pictory.ai/10-best-text-to-video-ai-tools/"; async function createBlogUrlToVideo() { try { console.log("Creating video from blog URL..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "blog_url_to_video", scenes: [ { blogUrl: BLOG_URL, // Blog article URL createSceneOnNewLine: true, // Create new scene on each new line createSceneOnEndOfSentence: true, // Create new scene at end of sentences } ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video from blog URL is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createBlogUrlToVideo(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample blog URL - replace with your own blog URL BLOG_URL = "https://blog.pictory.ai/10-best-text-to-video-ai-tools/" def create_blog_url_to_video(): try: print("Creating video from blog URL...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'blog_url_to_video', 'scenes': [ { 'blogUrl': BLOG_URL, # Blog article URL 'createSceneOnNewLine': True, # Create new scene on each new line 'createSceneOnEndOfSentence': True # Create new scene at end of sentences } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video from blog URL is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_blog_url_to_video() ``` ## Understanding the Parameters | Parameter | Type | Required | Description | | ------------------------------------- | ------- | -------- | ------------------------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `scenes` | array | Yes | Array of scene objects | | `scenes[].blogUrl` | string | Yes | The URL of the blog article or webpage to convert | | `scenes[].createSceneOnNewLine` | boolean | No | Create a new scene on each new line in the content (default: false) | | `scenes[].createSceneOnEndOfSentence` | boolean | No | Create a new scene at the end of each sentence (default: false) | ## Supported Content Types The API can extract and convert content from: | Content Type | Examples | Processing Notes | | ----------------- | ---------------------------------------------- | ------------------------------------------ | | Blog Posts | Personal blogs, company blogs, Medium articles | Best results with 300-2000 word articles | | News Articles | Online news sites, magazine articles | Main content extracted, ads filtered out | | WordPress Sites | WordPress-powered blogs and sites | Clean extraction from WordPress structure | | Content Platforms | Medium, Substack, LinkedIn articles | Platform-specific parsing for best results | | Static Web Pages | HTML pages, landing pages | Works with well-structured HTML content | **Access Requirements:** The URL must be publicly accessible without login, subscription, or paywall. Password-protected, members-only, or paywalled content cannot be extracted. ## Common Use Cases ### Content Marketing and Social Media ```javascript theme={null} { videoName: "latest_blog_post_social", scenes: [ { blogUrl: "https://yourblog.com/latest-marketing-tips", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Blog content repurposed as video for social media sharing. ### News and Updates ```javascript theme={null} { videoName: "company_news_announcement", scenes: [ { blogUrl: "https://company.com/news/quarterly-update", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** News article transformed into shareable video format. ### Educational Content ```javascript theme={null} { videoName: "tutorial_how_to_guide", scenes: [ { blogUrl: "https://knowledge-base.com/how-to-use-feature", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Tutorial article converted to video for better engagement. ### Product Announcements ```javascript theme={null} { videoName: "new_product_launch", scenes: [ { blogUrl: "https://blog.company.com/introducing-new-product", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Product announcement blog as promotional video. ## Best Practices Select articles that convert well to video: * **Clear Structure**: Articles with headings and paragraphs work best * **Appropriate Length**: 300-2000 words is ideal for video conversion * **Rich Content**: Articles with descriptive text help AI select better visuals * **Topic Focus**: Focused articles on specific topics convert better than general overviews * **Visual Language**: Descriptive, visual language helps the AI match relevant stock footage Ensure your URL will work: * **Public Access**: URL must be accessible without login (test in incognito browser) * **No Paywalls**: Content behind paywalls cannot be extracted * **Clean URLs**: Use the article URL, not shortened or tracking URLs * **Direct Link**: Link directly to the article, not category or archive pages * **Active Page**: Ensure the page hasn't been deleted or moved Prepare your articles for best conversion: * **Descriptive Text**: Use visual, descriptive language in your writing * **Logical Sections**: Break content into clear sections with headings * **Concise Paragraphs**: Shorter paragraphs (3-5 sentences) work better * **Key Points**: Highlight main ideas that translate well to visual scenes * **Active Voice**: Use active voice for more engaging narration Consider appropriate video duration: * **Short Articles (300-500 words)**: 1-2 minute videos * **Medium Articles (500-1000 words)**: 2-4 minute videos * **Long Articles (1000-2000 words)**: 4-6 minute videos * **Very Long Articles (2000+ words)**: May be automatically summarized * **Social Media**: Consider extracting key sections for shorter clips Experiment to find what works best: * **Start Simple**: Begin with shorter, well-structured articles * **Compare Platforms**: Different blogging platforms may yield different results * **Review Output**: Check generated videos to refine your content strategy * **Iterate**: Use insights from generated videos to improve future articles * **Document Success**: Note which article types and structures work best ## Troubleshooting **Problem:** The API cannot access or parse content from the URL. **Solution:** * Verify the URL is publicly accessible (test in incognito/private browser) * Check that the page does not require login or subscription * Ensure the URL points to an article, not a homepage or category page * Try removing URL parameters (everything after `?` in the URL) * Verify the page loads correctly and contains text content * Check if the site has anti-scraping protection or blocking **Problem:** Generated video does not include all the article content. **Solution:** * Check if the article has substantial text content (300+ words) * Verify the URL points to the full article, not a preview or excerpt * Very long articles may be automatically summarized - consider this expected * Ensure article structure is clear (the API filters out ads and navigation) * Try a different article to test if it is a page-specific issue **Problem:** Selected stock visuals seem unrelated to article content. **Solution:** * Ensure article contains descriptive, visual language * Add more context about key concepts in the article * The AI selects visuals based on extracted text - review article wording * Consider that visuals may be thematic rather than literal * Try articles with clearer, more specific topics **Problem:** Job status shows "in-progress" for extended periods. **Solution:** * Blog-to-video processing time depends on article length * Expected times: * Short articles (300-500 words): 5-10 minutes * Medium articles (500-1000 words): 10-15 minutes * Long articles (1000-2000 words): 15-25 minutes * Complex page structures may take longer to parse * Check status every 5-10 seconds (not more frequently) * If stuck for over 30 minutes, contact support with job ID **Problem:** Cannot extract content from subscription or paywall-protected articles. **Solution:** * The API cannot access paywalled or protected content * Options: * Copy article text and use the [Text to Video](/guides/text-to-video/text-to-video-basic) endpoint instead * Publish a public version of the article for video conversion * Use article preview/excerpt if publicly available * For members-only content, make it temporarily public for processing ## Next Steps Enhance your blog-to-video content with these features: Add AI narration to your blog videos Add music to complement your videos Apply consistent branding automatically Customize or translate subtitles ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Blog URL to Video with AI Voice-Over Source: https://docs.pictory.ai/guides/article-to-video/blog-with-voiceover Convert blog articles into videos with AI-generated voice-over narration This guide shows you how to convert blog articles into engaging videos with professional AI-generated voice-over narration. Perfect for repurposing written content into narrated video format for social media, YouTube, or podcasts. ## What You'll Learn Convert blog URLs to video storyboards automatically Add professional narration to blog videos Control speaker, speed, and amplification settings Voice-over syncs automatically with video scenes ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The URL of a publicly accessible blog article * Basic understanding of voice-over concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Blog-to-Video with Voice-Over Works When you convert a blog to video with voice-over narration: 1. **Content Extraction** - The API scrapes your blog URL for text content 2. **Scene Generation** - The content is analyzed and split into logical scenes 3. **Visual Selection** - Appropriate stock visuals are automatically matched to each scene 4. **Voice Generation** - AI creates natural voice-over narration from the extracted text 5. **Synchronization** - Voice-over is automatically synchronized with video timing 6. **Video Rendering** - The final video is rendered with narration and visuals combined The blog URL must be publicly accessible. Password-protected or paywalled content cannot be extracted. The AI will automatically determine the best way to structure your content into scenes. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample blog URL - replace with your own blog URL const BLOG_URL = "https://blog.pictory.ai/10-best-text-to-video-ai-tools/"; async function createBlogUrlToVideoWithVoiceOver() { try { console.log("Creating video from blog URL with AI voice-over..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "blog_url_to_video_with_voiceover", // Voice-over configuration voiceOver: { enabled: true, // Enable voice-over aiVoices: [ { speaker: "Brian", // AI voice name speed: 100, // Normal speaking speed (50-200) amplificationLevel: 0, // Normal volume (-1 to 1) }, ], }, scenes: [ { blogUrl: BLOG_URL, // Blog article URL createSceneOnNewLine: true, // Create new scene on each new line createSceneOnEndOfSentence: true, // Create new scene at end of sentences } ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video from blog URL with voice-over is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createBlogUrlToVideoWithVoiceOver(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample blog URL - replace with your own blog URL BLOG_URL = "https://blog.pictory.ai/10-best-text-to-video-ai-tools/" def create_blog_url_to_video_with_voiceover(): try: print("Creating video from blog URL with AI voice-over...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'blog_url_to_video_with_voiceover', # Voice-over configuration 'voiceOver': { 'enabled': True, # Enable voice-over 'aiVoices': [ { 'speaker': 'Brian', # AI voice name 'speed': 100, # Normal speaking speed (50-200) 'amplificationLevel': 0 # Normal volume (-1 to 1) } ] }, 'scenes': [ { 'blogUrl': BLOG_URL, # Blog article URL 'createSceneOnNewLine': True, # Create new scene on each new line 'createSceneOnEndOfSentence': True # Create new scene at end of sentences } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video from blog URL with voice-over is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_blog_url_to_video_with_voiceover() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ------------------------------------- | ------- | -------- | ------------------------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `voiceOver` | object | Yes | Voice-over configuration object | | `scenes` | array | Yes | Array of scene objects | | `scenes[].blogUrl` | string | Yes | The URL of the blog article or webpage to convert | | `scenes[].createSceneOnNewLine` | boolean | No | Create a new scene on each new line in the content (default: false) | | `scenes[].createSceneOnEndOfSentence` | boolean | No | Create a new scene at the end of each sentence (default: false) | ### Voice-Over Configuration | Parameter | Type | Required | Description | | -------------------- | ------- | -------- | --------------------------------------------------------------- | | `enabled` | boolean | Yes | Set to `true` to enable voice-over | | `aiVoices` | array | Yes | Array of AI voice configurations (currently supports one voice) | | `speaker` | string | Yes | The name of the AI voice (e.g., "Brian", "Emma") | | `speed` | number | No | Voice speed from 50-200 (default: 100 = normal speed) | | `amplificationLevel` | number | No | Volume level from -1 to 1 (default: 0 = normal volume) | To get a complete list of available AI voices, use the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) API endpoint. This returns all available voices with their names, languages, and characteristics. ## Voice Speed Reference | Speed Value | Playback Rate | Best Used For | | ----------- | -------------------------- | ------------------------------------------------- | | 50 | 0.5x (Very slow) | Complex technical content, detailed explanations | | 75 | 0.75x (Slower) | Educational content, learning materials | | 90 | 0.9x (Slightly slower) | Professional presentations, important information | | 100 | 1.0x (Normal) | Standard content, most blog articles | | 110-120 | 1.1-1.2x (Slightly faster) | Casual content, quick summaries | | 150 | 1.5x (Fast) | Quick recaps, energetic content | | 200 | 2.0x (Very fast) | Speed reading, urgent updates | ## Amplification Level Reference | Level | Effect | Best Used For | | ----- | ------------------- | -------------------------------------------- | | -1.0 | Quietest | Background narration, ambient voice | | -0.5 | Quieter than normal | Subtle emphasis, secondary information | | 0 | Normal volume | Standard narration, most use cases | | 0.3 | Slightly louder | Important points, key messages | | 0.5 | Moderately louder | Strong emphasis, calls-to-action | | 1.0 | Loudest | Maximum emphasis, attention-grabbing moments | **Volume Considerations:** Very high amplification levels (0.7-1.0) may cause audio distortion. Test your settings and use moderation for professional results. ## Supported Content Types The API can extract and convert content from: * Blog posts and articles * News websites * Medium articles * WordPress sites * Most publicly accessible web pages * Content management system (CMS) pages The URL must be publicly accessible without authentication. Password-protected, paywalled, or login-required content cannot be extracted. ## Common Use Cases ### Content Marketing and Social Media ```javascript theme={null} { videoName: "social_media_video", voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", // Female voice for lifestyle content speed: 110, // Slightly faster for engaging delivery amplificationLevel: 0.2 // Slightly louder for emphasis }] }, scenes: [ { blogUrl: "https://yourblog.com/latest-article", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Engaging social media video with professional female narration. ### Educational and Tutorial Content ```javascript theme={null} { videoName: "tutorial_video", voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", // Clear male voice for tutorials speed: 90, // Slower for comprehension amplificationLevel: 0 // Normal volume }] }, scenes: [ { blogUrl: "https://tutorial-site.com/how-to-guide", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Clear instructional video with slower narration for better understanding. ### Business and Corporate Content ```javascript theme={null} { videoName: "corporate_update", voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", // Professional male voice speed: 100, // Normal pace amplificationLevel: 0 // Standard volume }] }, scenes: [ { blogUrl: "https://company-blog.com/quarterly-update", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Professional corporate video with natural narration. ### Quick News and Updates ```javascript theme={null} { videoName: "news_update", voiceOver: { enabled: true, aiVoices: [{ speaker: "Joanna", // Dynamic female voice speed: 120, // Faster for news delivery amplificationLevel: 0.3 // Louder for energy }] }, scenes: [ { blogUrl: "https://news-site.com/latest-update", createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ] } ``` **Result:** Energetic news-style video with faster delivery. ## Best Practices Match voice speed to your content type and audience: * **Technical/Complex**: Use 75-90 for slower, clearer delivery * **Standard Content**: Use 100 for natural, comfortable pacing * **Casual/Social**: Use 110-120 for energetic, engaging delivery * **Quick Updates**: Use 120-150 for fast-paced content * **Test First**: Always preview before production to ensure the pace feels right Use volume strategically for emphasis: * **Subtle Changes**: Use 0.1-0.3 for gentle emphasis on key points * **Normal Content**: Keep at 0 for most narration * **Strong Emphasis**: Use 0.4-0.6 sparingly for critical messages * **Avoid Extremes**: Don't exceed 0.7 to prevent audio distortion * **Consistency**: Maintain similar levels across similar content types Choose voices that match your brand and content: * **Professional/Business**: Brian, Matthew, or Joanna * **Casual/Friendly**: Emma, Amy, or Joey * **Technical/Educational**: Brian or Emma for clarity * **Brand Consistency**: Use the same voice across all your videos * **Test Options**: Try different voices to find the best fit Use the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks) to explore all available voices. Ensure your blog URL will work: * **Public Access**: URL must be accessible without login * **No Paywalls**: Content behind paywalls cannot be extracted * **Clean URLs**: Avoid URLs with session parameters or tracking codes * **Test First**: Manually check the URL opens in a browser * **Well-Structured**: Articles with clear HTML structure work best Prepare your blog for better video conversion: * **Clear Structure**: Use headings and paragraphs effectively * **Concise Writing**: Shorter sentences work better for narration * **Relevant Length**: 500-2000 words is ideal for video conversion * **Visual Content**: Include images that can be used in the video * **Clean Formatting**: Remove excessive ads or sidebars when possible ## Troubleshooting **Problem:** The API cannot access or parse the blog content. **Solution:** * Verify the URL is publicly accessible (test in incognito browser) * Check that the article does not require login or subscription * Ensure the URL is complete and correctly formatted * Try removing URL parameters (everything after `?`) * Verify the blog page loads correctly and has text content * Check if the site has anti-scraping protection **Problem:** The voice-over sounds artificial or choppy. **Solution:** * Try a different AI voice speaker (some sound more natural) * Adjust the speed to 95-105 for more natural cadence * Ensure your blog content has proper punctuation * Avoid using all caps or unusual formatting in source content * Use the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks) to find higher-quality voices **Problem:** Narration is too fast/slow for the video scenes. **Solution:** * The API automatically syncs voice to video duration * If issues persist, adjust the `speed` parameter: * Increase speed (110-120) if narration is too slow * Decrease speed (80-90) if narration is too fast * Very long articles may be automatically summarized * Consider breaking long articles into multiple videos **Problem:** Voice-over audio sounds muffled or distorted. **Solution:** * Reduce `amplificationLevel` to 0 or below * Avoid levels above 0.7 which can cause clipping * Try a different AI voice - some have better audio quality * Check if the source content has unusual characters or formatting * Ensure punctuation in source content is correct **Problem:** Video completes but appears empty or very short. **Solution:** * Check that the URL points to an article, not the homepage * Verify the page has substantial text content (300+ words) * Remove URL parameters and tracking codes * Try a direct link to the article, not a shortened URL * Ensure the page is in a supported language ## Next Steps Enhance your blog-to-video content with these features: Add music to complement voice-over narration Add translated or custom subtitles to your videos Apply consistent branding across all your videos Use different voices for different scenes ## API Reference For complete technical details, see: * [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) - List all available AI voices * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Podcast/Audio to Video Source: https://docs.pictory.ai/guides/audio-to-video/podcast-to-video Convert podcast episodes or audio files into engaging videos with automated visuals This guide shows you how to convert podcast episodes or audio files into engaging videos with automatically generated visuals. Perfect for repurposing audio content for YouTube, social media, or video platforms. ## What You'll Learn Convert podcasts and audio files to videos Automatically transcribe audio content Generate visuals synchronized to audio Auto-generated subtitles from transcription ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Audio file accessible via public URL * Basic understanding of audio/podcast formats ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Podcast-to-Video Works When you convert audio to video: 1. **Audio Processing** - Your audio file is accessed and analyzed 2. **Transcription** - AI automatically transcribes the audio content 3. **Scene Generation** - Content is split into logical scenes based on the transcript 4. **Visual Selection** - Appropriate stock visuals are selected to match the audio content 5. **Caption Generation** - Subtitles are created from the transcription 6. **Video Rendering** - Final video is assembled with audio, visuals, and captions synchronized The audio file must be accessible via a public URL. Upload to cloud storage (Google Drive, Dropbox, AWS S3) and use the public share link. Processing time is proportional to audio length. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample audio URL - replace with your own audio file URL const AUDIO_URL = "https://pictory-static.pictorycontent.com/sample_podcast.mp3"; async function createPodcastToVideo() { try { console.log("Creating video from podcast/audio..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "podcast_to_video", scenes: [ { audioUrl: AUDIO_URL, // Audio file URL at scene level audioLanguage: "en-US", // Required for audio processing } ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video from podcast is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createPodcastToVideo(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample audio URL - replace with your own audio file URL AUDIO_URL = "https://pictory-static.pictorycontent.com/sample_podcast.mp3" def create_podcast_to_video(): try: print("Creating video from podcast/audio...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'podcast_to_video', 'scenes': [ { 'audioUrl': AUDIO_URL, # Audio file URL at scene level 'audioLanguage': 'en-US' # Required for audio processing } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video from podcast is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_podcast_to_video() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `scenes` | array | Yes | Array of scene objects | ### Scene Parameters | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------- | | `audioUrl` | string | Yes | Public URL to the audio file (podcast, interview, etc.) | | `audioLanguage` | string | Yes | Language code for audio transcription (e.g., "en-US") | ## Supported Audio Formats | Format | Extension | Description | | ------ | --------- | ---------------------------------------- | | MP3 | `.mp3` | Most common podcast format (recommended) | | WAV | `.wav` | Uncompressed audio, high quality | | M4A | `.m4a` | Apple audio format, good quality | | AAC | `.aac` | Advanced audio codec | | FLAC | `.flac` | Lossless audio compression | | OGG | `.ogg` | Open-source audio format | **Best Format:** Use MP3 for the best balance of quality and processing speed. Ensure audio is mono or stereo with clear speech. ## Common Use Cases ### Podcast Episodes for YouTube ```javascript theme={null} { videoName: "podcast_episode_042_youtube", scenes: [{ audioUrl: "https://storage.example.com/episode-042.mp3", audioLanguage: "en-US" }] } ``` **Result:** Full podcast episode as video with matching visuals and captions. ### Interview Clips for Social Media ```javascript theme={null} { videoName: "interview_highlight_linkedin", scenes: [{ audioUrl: "https://storage.example.com/interview-segment.mp3", audioLanguage: "en-US" }] } ``` **Result:** Short interview clip with professional visuals for LinkedIn. ### Audio Blog Posts ```javascript theme={null} { videoName: "audio_blog_marketing_tips", scenes: [{ audioUrl: "https://storage.example.com/blog-audio.mp3", audioLanguage: "en-US" }] } ``` **Result:** Audio blog converted to video format with relevant visuals. ### Webinar Audio Archives ```javascript theme={null} { videoName: "webinar_q_and_a_session", scenes: [{ audioUrl: "https://storage.example.com/webinar-audio.m4a", audioLanguage: "en-US" }] } ``` **Result:** Webinar audio transformed into watchable video content. ## Best Practices Ensure high-quality audio for best transcription: * **Clear Speech**: Use good microphones for recording * **Minimize Noise**: Reduce background noise and echo * **Proper Levels**: Avoid audio that is too quiet or distorted * **Single Speaker**: One speaker at a time works best for transcription * **Speaking Pace**: Natural speaking pace (not too fast) improves accuracy * **File Quality**: Use 128kbps or higher bitrate for MP3 Match content length to your platform and audience: * **Social Media**: Extract 1-3 minute clips for maximum engagement * **YouTube**: 5-15 minutes works well for episodic content * **Full Episodes**: Consider breaking 30+ minute podcasts into segments * **Processing Time**: Longer audio = longer processing (plan accordingly) * **Viewer Retention**: Shorter clips often perform better on social platforms Ensure your audio file can be accessed: * **Cloud Storage**: Upload to Google Drive, Dropbox, or AWS S3 * **Public Link**: Generate a direct, public download link * **Test Access**: Verify link works in incognito browser * **Stable URL**: Ensure link will not expire during processing * **Direct URL**: Use direct file URL, not streaming or preview links Improve AI transcription results: * **Clear Audio**: Clean recordings transcribe more accurately * **Standard Accents**: Clear, standard pronunciation works best * **Avoid Jargon**: Technical terms may not transcribe perfectly * **Speaker Separation**: Clear pauses between speakers help * **Background Music**: Minimize or remove background music for better transcription Create focused, engaging content: * **Highlight Reels**: Extract best moments from full episodes * **Topic Segments**: Create separate videos for different topics * **Intro Clips**: Use episode intros as social media teasers * **Quotes**: Extract powerful quotes or insights as short clips * **Series**: Create a series of related short videos from one episode ## Troubleshooting **Problem:** The API cannot download or process your audio file. **Solution:** * Verify the URL is publicly accessible (test in incognito browser) * Ensure it is a direct download link, not a streaming or preview link * For Google Drive: Right-click → Share → "Anyone with the link" → Copy link * For Dropbox: Share → Create link → change "dl=0" to "dl=1" at end of URL * Check file hasn't been deleted or moved * Verify file format is supported (see table above) **Problem:** Auto-generated captions do not match the audio. **Solution:** * Improve audio quality (reduce background noise, use better microphone) * Ensure speakers speak clearly and at moderate pace * Reduce background music volume if present * Check audio is not distorted or too quiet * Try re-recording with better equipment/conditions * Consider professional audio editing before conversion **Problem:** Selected stock visuals seem unrelated to podcast topic. **Solution:** * The AI selects visuals based on transcribed content * Ensure speakers mention key visual concepts in the audio * More descriptive language helps AI select better visuals * Consider extracting specific segments with clearer topics * Review final video - sometimes visuals are thematic rather than literal **Problem:** Job status shows "in-progress" for extended periods. **Solution:** * Audio processing time depends on length and quality * Expected times: * 1-5 minutes audio: 5-10 minutes processing * 10-30 minutes audio: 15-30 minutes processing * 60+ minutes audio: 45-90 minutes processing * Large file sizes take longer to download and process * Check status every 5-10 seconds, not more frequently * If stuck for 2x expected time, contact support with job ID **Problem:** Expected auto-generated captions but they are missing. **Solution:** * Captions are automatically generated from transcription * Verify audio contains clear, audible speech * Check that audio is not purely music or sound effects * Ensure audio file is not corrupted * Try with a different audio file to test * Contact support if issue persists ## Next Steps Enhance your podcast videos with these features: Add music layers to your podcast videos Customize or translate auto-generated captions Apply consistent branding to all videos Add branded intro and outro sequences ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Using Brand Settings Source: https://docs.pictory.ai/guides/branding-customization/brand-settings Apply saved brand presets to your videos for consistent branding across all content This guide shows you how to apply saved brand presets to your videos for consistent branding across all your content. Brand presets include logos, colors, fonts, subtitle styles, and other brand elements saved in your Pictory account. ## What You'll Learn Use saved brand configurations by name Apply uniform styling across all videos Speed up video creation with presets Manage multiple brand identities ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Brand preset created in your Pictory account * Brand preset name from your Pictory settings ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Brand Presets Work When you apply a brand preset to your video: 1. **Brand Selection** - You specify your brand preset by name 2. **Style Application** - All brand settings are automatically applied 3. **Consistent Elements** - Logos, colors, fonts, and styles are unified 4. **Automatic Formatting** - Subtitles, intro/outro, and layouts use brand settings 5. **Video Creation** - Your video is created with complete brand consistency Brand presets must be created in your Pictory account before using them via the API. You can create and manage brand presets through the Pictory web interface under Brand Settings. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createVideoWithBrandPreset() { try { console.log("Creating video with brand preset..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_with_brand", brandName: "{YOUR_BRAND_NAME}", // Your saved brand preset name // Scene configuration scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with brand preset is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithBrandPreset(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_video_with_brand_preset(): try: print("Creating video with brand preset...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_with_brand', 'brandName': '{YOUR_BRAND_NAME}', # Your saved brand preset name # Scene configuration 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with brand preset is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_brand_preset() ``` ## Understanding the Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------ | | `videoName` | string | Yes | A descriptive name for your video project | | `brandName` | string | Yes | Exact name of your saved brand preset from Pictory account | | `scenes` | array | Yes | Video scene configuration (story text, scene creation rules) | **Brand Name is Case-Sensitive:** The brand name must exactly match the name saved in your Pictory account, including capitalization and spacing. ## Brand Preset Components Brand presets automatically apply these elements to your videos: | Element | What It Includes | Example | | ------------------- | ------------------------------------- | ---------------------------------- | | **Logo** | Position, size, and placement | Company logo in top-right corner | | **Colors** | Primary and secondary brand colors | Specific hex codes for consistency | | **Fonts** | Text styling and typography | Branded font families and sizes | | **Intro/Outro** | Branded opening and closing sequences | 5-second branded intro video | | **Subtitle Styles** | Consistent caption formatting | Font, color, position, animations | | **Voice Settings** | Preferred AI voice configuration | Specific voice, speed, volume | ## Finding Your Brand Presets ### Option 1: Using the Pictory Web Interface 1. Log in to your Pictory account 2. Navigate to **Brand Kits** or **Brand Settings** 3. View your saved brand presets 4. Note the exact brand name to use in your API calls ### Option 2: Using the API Retrieve all your brand presets programmatically: ```javascript Node.js theme={null} const brandsResponse = await axios.get( `${API_BASE_URL}/v1/brands/video`, { headers: { Authorization: API_KEY } } ); console.log('Your brand presets:'); brandsResponse.data.forEach(brand => { console.log(`- Name: ${brand.name}`); console.log(` ID: ${brand.id}`); }); // Use the 'name' field in your video creation requests ``` ```python Python theme={null} brands_response = requests.get( f'{API_BASE_URL}/v1/brands/video', headers={'Authorization': API_KEY} ) print('Your brand presets:') for brand in brands_response.json(): print(f"- Name: {brand['name']}") print(f" ID: {brand['id']}") # Use the 'name' field in your video creation requests ``` See [Get Video Brands](/api-reference/branding/get-video-brands) for complete API documentation. ## Common Use Cases ### Maintaining Brand Consistency ```javascript theme={null} { videoName: "company_announcement", brandName: "Corporate Brand" } ``` **Result:** All company videos use identical branding, ensuring professional consistency. ### Quick Video Production ```javascript theme={null} { videoName: "social_media_post", brandName: "Social Media Brand" } ``` **Result:** Instantly create videos with pre-configured social media styling. ### Multi-Brand Management ```javascript theme={null} // Client A video { brandName: "Client A Brand" } // Client B video { brandName: "Client B Brand" } ``` **Result:** Agencies can manage multiple client brands easily with separate presets. ### Team Collaboration ```javascript theme={null} { videoName: "team_training", brandName: "Internal Training Brand" } ``` **Result:** All team members use the same brand guidelines automatically. ## Best Practices Organize brand presets for different use cases: * **Corporate:** Professional presentations and announcements * **Social Media:** Casual content for Instagram, TikTok, LinkedIn * **Training:** Educational and internal training videos * **Marketing:** Promotional and sales videos * **Events:** Conference and webinar content This allows you to quickly select the appropriate branding for each video type. Name your brand presets clearly: * **Good:** "Corporate - Blue Theme", "Social - Casual Style" * **Avoid:** "Brand1", "Test", "New" Clear names make it easier to select the right preset and maintain your API code. Before using a brand preset via the API: * Create a test video in the Pictory web interface * Verify all brand elements appear correctly * Confirm colors, fonts, and logos are as expected * Make any adjustments in the brand settings * Then use the finalized preset in your API calls Maintain your brand presets over time: * Update logos when branding changes * Refresh color schemes seasonally if needed * Adjust subtitle styles based on platform trends * Test updated presets before using in production * Document changes for your team Create a reference guide for your team: * List all available brand presets with descriptions * Specify which preset to use for which content type * Include example API requests for common scenarios * Note any special requirements or restrictions * Update documentation when adding new presets ## Troubleshooting **Problem:** API returns an error that the brand preset does not exist. **Solution:** * Verify the brand name is spelled exactly as saved in Pictory * Check for case sensitivity (e.g., "Corporate" vs "corporate") * Use the Get Video Brands API to see all available presets * Ensure the brand preset exists in your account * Try copying the name directly from the Pictory interface **Problem:** Video does not show logo, colors, or other brand elements. **Solution:** * Check that the brand preset is fully configured in Pictory * Verify all brand elements are saved in the preset * Test the preset in the Pictory web interface first * Ensure the preset includes the specific elements you expect * Review the completed video - some elements may be subtle **Problem:** Brand works in Pictory interface but not through API calls. **Solution:** * Confirm you are using the exact brand name (check spelling and case) * Use the Get Video Brands API to retrieve the correct name * Ensure your API key has access to that brand preset * Check that the preset has not been deleted or renamed * Verify no special characters or extra spaces in the name **Problem:** Video uses a different brand preset than specified. **Solution:** * Double-check the `brandName` parameter in your request * Ensure you are not overriding brand elements with other parameters * Verify the brand name is in quotes and properly formatted * Check for typos or autocomplete errors in your code * Review the request payload before sending **Problem:** Get Video Brands API returns empty or errors. **Solution:** * Confirm you have created brand presets in your Pictory account * Check that your API key is valid and active * Ensure you are using the correct API endpoint * Verify your account has access to brand features * Contact support if the issue persists ## Next Steps Explore more branding customization options: Add and position logos without brand presets Apply saved subtitle styling to videos Create inline subtitle styles without presets Emphasize important words in subtitles ## API Reference For complete technical details, see: * [Get Video Brands](/api-reference/branding/get-video-brands) - List all your brand presets * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Custom Subtitle Styling Source: https://docs.pictory.ai/guides/branding-customization/custom-subtitle-style Create videos with fully customized inline subtitle styles without saving presets This guide shows you how to create videos with completely custom subtitle styles defined directly in your API request. Instead of using saved presets, you can specify all styling properties inline for maximum flexibility. ## What You'll Learn Define custom styles directly in API requests Customize fonts, colors, positions, and effects Style without saving configurations Add entry and exit animations ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Basic understanding of colors (RGBA format) * Familiarity with subtitle positioning ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Custom Subtitle Styling Works When you define subtitle styles inline: 1. **Style Definition** - You specify all subtitle properties in your request 2. **Property Application** - Font, colors, position, and effects are configured 3. **Animation Setup** - Entry and exit animations are applied (optional) 4. **No Saving Required** - Style exists only for this video (not saved to account) 5. **Video Rendering** - Subtitles render with your exact specifications Inline subtitle styles give you complete control without creating saved presets. This is perfect for one-time custom styling or testing different subtitle configurations. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media."; async function createVideoWithCustomSubtitleStyle() { try { console.log("Creating video with custom subtitle style..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_custom_subtitle", // Custom inline subtitle style subtitleStyle: { // Font settings fontFamily: "Poppins", fontSize: 48, // Colors (RGBA format) color: "rgba(255, 255, 255, 1)", // White text backgroundColor: "rgba(0, 0, 0, 0.7)", // Semi-transparent black background keywordColor: "rgba(255, 215, 0, 1)", // Gold for keywords shadowColor: "rgba(0, 0, 0, 0.8)", // Dark shadow shadowWidth: "2%", // Position and layout position: "bottom-center", alignment: "center", paragraphWidth: "80%", // Text styling decorations: ["bold"], case: "capitalize", // Animations animations: [ { name: "fade", type: "entry", speed: "medium", }, { name: "fade", type: "exit", speed: "fast", }, ], }, scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with custom subtitle style is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithCustomSubtitleStyle(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media." def create_video_with_custom_subtitle_style(): try: print("Creating video with custom subtitle style...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_custom_subtitle', # Custom inline subtitle style 'subtitleStyle': { # Font settings 'fontFamily': 'Poppins', 'fontSize': 48, # Colors (RGBA format) 'color': 'rgba(255, 255, 255, 1)', # White text 'backgroundColor': 'rgba(0, 0, 0, 0.7)', # Semi-transparent black background 'keywordColor': 'rgba(255, 215, 0, 1)', # Gold for keywords 'shadowColor': 'rgba(0, 0, 0, 0.8)', # Dark shadow 'shadowWidth': '2%', # Position and layout 'position': 'bottom-center', 'alignment': 'center', 'paragraphWidth': '80%', # Text styling 'decorations': ['bold'], 'case': 'capitalize', # Animations 'animations': [ {'name': 'fade', 'type': 'entry', 'speed': 'medium'}, {'name': 'fade', 'type': 'exit', 'speed': 'fast'} ] }, 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with custom subtitle style is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_custom_subtitle_style() ``` ## Subtitle Style Parameters ### Font Properties | Property | Type | Description | Example | | ------------ | ------ | -------------------------- | ------------------------------------------------------------------ | | `fontFamily` | string | Font name | "Poppins", "Arial", "Roboto" | | `fontUrl` | string | Custom font URL (optional) | "[https://example.com/font.woff2](https://example.com/font.woff2)" | | `fontSize` | number | Font size in pixels | 48, 36, 60 | ### Color Properties All colors use **RGBA format**: `rgba(R, G, B, A)` where R/G/B are 0-255 and A is 0-1 | Property | Description | Example | | ----------------- | ------------------------------ | --------------------------------------------- | | `color` | Main text color | `rgba(255, 255, 255, 1)` (white) | | `backgroundColor` | Background behind text | `rgba(0, 0, 0, 0.7)` (semi-transparent black) | | `keywordColor` | Color for highlighted keywords | `rgba(255, 215, 0, 1)` (gold) | | `shadowColor` | Text shadow color | `rgba(0, 0, 0, 0.8)` (dark gray) | ### Position and Layout | Property | Type | Options/Description | | ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `position` | string | `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` | | `alignment` | string | `left`, `center`, `right` | | `paragraphWidth` | string | Percentage of screen width (e.g., "80%", "100%") | | `shadowWidth` | string | Shadow size as percentage (e.g., "2%", "5%") | ### Text Styling | Property | Type | Options | | ------------- | ------ | ------------------------------------------------------------------------------------ | | `decorations` | array | `["bold"]`, `["underline"]`, `["italics"]`, `["linethrough"]` (can combine multiple) | | `case` | string | `uppercase`, `lowercase`, `capitalize`, `smallcapitalize` | ## Animation Options You can add up to 2 animations per subtitle style (one entry, one exit). ### Available Animations | Animation Name | Type | Description | | -------------- | ---------- | ----------------------------------- | | `none` | Entry/Exit | No animation | | `fade` | Entry/Exit | Gradual fade in or out | | `drift` | Entry/Exit | Drift motion with fade | | `wipe` | Entry/Exit | Wipe across screen | | `text reveal` | Entry | Text reveals character by character | | `elastic` | Entry | Elastic bounce effect | | `typewriter` | Entry | Typewriter effect | | `blur` | Entry/Exit | Blur transition | | `bulletin` | Entry | Bulletin board style | ### Animation Configuration | Property | Options | Description | | ----------- | ------- | ---------------------------------------------------------- | | `name` | string | Animation name from table above | | `type` | string | `entry` (appearing) or `exit` (disappearing) | | `speed` | string | `slow`, `medium`, `fast`, `custom` | | `direction` | string | `up`, `down`, `left`, `right` (for directional animations) | ## RGBA Color Format ``` rgba(R, G, B, A) ``` | Component | Range | Description | | ------------- | ----- | ------------------------------------------ | | **R** (Red) | 0-255 | Red intensity | | **G** (Green) | 0-255 | Green intensity | | **B** (Blue) | 0-255 | Blue intensity | | **A** (Alpha) | 0-1 | Transparency (0 = transparent, 1 = opaque) | ### Common Colors | Color | RGBA Value | | ---------------------- | ------------------------ | | White | `rgba(255, 255, 255, 1)` | | Black | `rgba(0, 0, 0, 1)` | | Semi-transparent black | `rgba(0, 0, 0, 0.7)` | | Gold | `rgba(255, 215, 0, 1)` | | Red | `rgba(255, 0, 0, 1)` | | Blue | `rgba(0, 120, 255, 1)` | | Green | `rgba(34, 197, 94, 1)` | Use online RGBA color pickers to easily create color values for your custom styles. ## Common Use Cases ### Professional Business Subtitles ```javascript theme={null} { subtitleStyle: { fontFamily: "Roboto", fontSize: 42, color: "rgba(255, 255, 255, 1)", backgroundColor: "rgba(0, 0, 0, 0.8)", position: "bottom-center", decorations: ["bold"] } } ``` **Result:** Clean, professional subtitles with strong readability. ### Social Media Style ```javascript theme={null} { subtitleStyle: { fontFamily: "Poppins", fontSize: 56, color: "rgba(255, 255, 255, 1)", keywordColor: "rgba(255, 215, 0, 1)", backgroundColor: "rgba(0, 0, 0, 0)", shadowColor: "rgba(0, 0, 0, 1)", shadowWidth: "3%", position: "center-center", case: "uppercase", decorations: ["bold"] } } ``` **Result:** Eye-catching subtitles perfect for TikTok, Instagram Reels. ### Educational Content ```javascript theme={null} { subtitleStyle: { fontFamily: "Arial", fontSize: 40, color: "rgba(0, 0, 0, 1)", backgroundColor: "rgba(255, 255, 255, 0.9)", position: "bottom-center", alignment: "center", paragraphWidth: "90%", decorations: [] } } ``` **Result:** High-contrast, easy-to-read subtitles for learning materials. ### Animated Dynamic Subtitles ```javascript theme={null} { subtitleStyle: { fontFamily: "Montserrat", fontSize: 48, color: "rgba(255, 255, 255, 1)", backgroundColor: "rgba(0, 0, 0, 0.6)", position: "bottom-center", animations: [ { name: "typewriter", type: "entry", speed: "fast" }, { name: "fade", type: "exit", speed: "medium" } ] } } ``` **Result:** Engaging subtitles with typewriter entry and fade exit. ## Best Practices Select fonts and sizes for optimal readability: * **Font Size:** 36-56 pixels for most videos (1920x1080) * **Font Families:** Use web-safe fonts like Poppins, Roboto, Arial, Montserrat * **Font Weight:** Bold is generally more readable on video * **Custom Fonts:** Test custom fonts thoroughly before production * **Consistency:** Use same font family across all scenes for cohesion Ensure subtitles are easily visible: * **Light Text on Dark:** White text on semi-transparent black (most common) * **Dark Text on Light:** Black text on white background (high brightness videos) * **Shadow for Depth:** Add shadows to text without backgrounds * **Keyword Colors:** Use contrasting colors for keyword highlights * **Test Backgrounds:** Verify readability on various scene backgrounds Place subtitles where they are visible but not intrusive: * **Bottom-Center:** Standard position, does not cover main content * **Avoid Top:** Unless content is primarily at bottom * **Width:** 70-90% paragraph width prevents text from reaching edges * **Alignment:** Center alignment works best for most content * **Scene-Specific:** Consider different positions for different scene types Use animations to enhance, not distract: * **Entry Animation:** Fade or drift work well for most content * **Exit Animation:** Faster exit (fade) than entry feels natural * **Speed:** Medium or fast speeds prevent slow pacing * **Avoid Overuse:** Too many animations can be distracting * **Match Content:** Playful animations for casual, simple for professional Always preview your custom styles: * **Create Test Video:** Use short sample text first * **Multiple Scenes:** Test with different background colors * **Device Testing:** View on mobile and desktop * **Readability Check:** Ensure text is clear at different video sizes * **Adjust Iteratively:** Refine style based on test results ## Troubleshooting **Problem:** Text is not clearly visible in the video. **Solution:** * Increase background opacity (higher alpha value in backgroundColor) * Use higher contrast between text color and background * Add or increase shadow (shadowColor and shadowWidth) * Increase font size (try 48-56 pixels) * Use bold decoration for thicker, more visible text **Problem:** Subtitle colors appear different than intended. **Solution:** * Verify RGBA format is correct (rgba(R, G, B, A)) * Check RGB values are 0-255, alpha is 0-1 * Ensure proper comma separation in RGBA values * Test colors with online RGBA picker before using * Remember alpha channel affects transparency * Check for typos in color values **Problem:** Specified font is not being used. **Solution:** * Use standard web fonts (Poppins, Roboto, Arial, Montserrat) * Check font name spelling exactly * If using custom font URL, verify URL is accessible * Ensure fontUrl points directly to font file (.woff2, .woff, .ttf) * Test with standard font first, then add custom **Problem:** Entry or exit animations do not appear. **Solution:** * Verify animation name is from available options * Check type is either "entry" or "exit" * Ensure speed is "slow", "medium", "fast", or "custom" * Maximum 2 animations allowed (one entry, one exit) * Check animations array syntax is correct * Try simple "fade" animation first to test **Problem:** Subtitles appear in wrong location on screen. **Solution:** * Verify position value matches available options * Position names are case-sensitive and hyphenated * Check for typos: "bottom-center" not "bottom center" * Ensure paragraphWidth does not push text off-screen * Test with "bottom-center" first as baseline * Review final video to verify placement ## Next Steps Explore more subtitle and branding options: Use saved subtitle style presets Automatically emphasize important words Apply complete brand presets to videos Add multilingual captions to videos ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Highlight Keywords Source: https://docs.pictory.ai/guides/branding-customization/highlight-keywords Emphasize important words in subtitles with automatic AI-powered keyword highlighting This guide shows you how to automatically highlight important words in your video subtitles using AI-powered keyword detection. Perfect for emphasizing key points, product names, or calls-to-action in your videos. ## What You'll Learn Automatically emphasize important words AI identifies key terms automatically Show or hide subtitles per scene Works with any subtitle style ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Basic understanding of subtitle styling * Content with keywords worth highlighting ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Keyword Highlighting Works When you enable keyword highlighting: 1. **AI Analysis** - The AI analyzes text content in each scene 2. **Keyword Detection** - Important words are identified (nouns, key verbs, concepts) 3. **Color Application** - Keywords display in the specified `keywordColor` 4. **Regular Text** - Other words appear in standard subtitle `color` 5. **Video Rendering** - Subtitles render with highlighted keywords Keyword highlighting uses AI to automatically identify important words. You do not need to manually specify which words to highlight - the AI determines this based on content analysis. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media."; const STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency."; async function createVideoWithKeywordHighlighting() { try { console.log("Creating video with keyword highlighting..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_highlight_keywords", // Subtitle style with keyword color subtitleStyle: { fontFamily: "Poppins", fontSize: 48, color: "rgba(255, 255, 255, 1)", // White for regular text backgroundColor: "rgba(0, 0, 0, 0.7)", // Black background keywordColor: "rgba(255, 215, 0, 1)", // Gold for highlighted keywords position: "bottom-center", }, scenes: [ { story: STORY_TEXT_1, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, highlightKeywords: true, // Enable keyword highlighting hideSubtitles: false, // Show subtitles }, { story: STORY_TEXT_2, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, highlightKeywords: false, // No highlighting for this scene hideSubtitles: true, // Hide subtitles for this scene }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with keyword highlighting is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithKeywordHighlighting(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media." STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency." def create_video_with_keyword_highlighting(): try: print("Creating video with keyword highlighting...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_highlight_keywords', # Subtitle style with keyword color 'subtitleStyle': { 'fontFamily': 'Poppins', 'fontSize': 48, 'color': 'rgba(255, 255, 255, 1)', # White for regular text 'backgroundColor': 'rgba(0, 0, 0, 0.7)', # Black background 'keywordColor': 'rgba(255, 215, 0, 1)', # Gold for highlighted keywords 'position': 'bottom-center' }, 'scenes': [ { 'story': STORY_TEXT_1, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'highlightKeywords': True, # Enable keyword highlighting 'hideSubtitles': False # Show subtitles }, { 'story': STORY_TEXT_2, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, 'highlightKeywords': False, # No highlighting for this scene 'hideSubtitles': True # Hide subtitles for this scene } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with keyword highlighting is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_keyword_highlighting() ``` ## Understanding the Parameters ### Scene-Level Parameters | Parameter | Type | Default | Description | | ------------------- | ------- | ------- | ----------------------------------------------------- | | `highlightKeywords` | boolean | false | Enable/disable AI keyword highlighting for this scene | | `hideSubtitles` | boolean | false | Show/hide all subtitles for this scene | ### Required Style Property | Parameter | Type | Description | | ---------------------------- | ------ | ------------------------------------------------------------------ | | `subtitleStyle.keywordColor` | string | RGBA color for highlighted keywords (e.g., `rgba(255, 215, 0, 1)`) | **Keyword Color Required:** You must define `keywordColor` in your `subtitleStyle` for highlighting to be visible. Without this color specification, keywords will not appear different from regular text. ## What Gets Highlighted The AI automatically identifies and highlights: | Word Type | Examples | Typical Highlighting | | ---------------------- | ------------------------------------ | ------------------------------ | | **Key Nouns** | AI, educators, social media, content | Important subjects and objects | | **Action Verbs** | impact, automate, enhance, create | Significant actions | | **Important Concepts** | generation, consistency, efficiency | Core ideas | | **Product Names** | Brand names, specific products | Proper nouns | | **Numbers** | Statistics, percentages, quantities | Numerical data | | **Call-to-Action** | subscribe, buy, learn, join | Action words | The AI is trained to identify contextually important words. The same word might be highlighted in one context but not another, depending on its importance to the message. ## Common Use Cases ### Educational Content with Key Terms ```javascript theme={null} { subtitleStyle: { color: "rgba(255, 255, 255, 1)", keywordColor: "rgba(100, 200, 255, 1)", // Light blue for terms backgroundColor: "rgba(0, 0, 0, 0.7)" }, scenes: [{ story: "Machine learning algorithms analyze data patterns.", highlightKeywords: true }] } ``` **Result:** "Machine learning", "algorithms", "data patterns" highlighted in blue. ### Marketing with Product Names ```javascript theme={null} { subtitleStyle: { color: "rgba(255, 255, 255, 1)", keywordColor: "rgba(255, 100, 100, 1)", // Red for emphasis backgroundColor: "rgba(0, 0, 0, 0.8)" }, scenes: [{ story: "Try our Premium Plan for unlimited videos.", highlightKeywords: true }] } ``` **Result:** "Premium Plan", "unlimited videos" highlighted in red. ### Social Media Call-to-Action ```javascript theme={null} { subtitleStyle: { color: "rgba(255, 255, 255, 1)", keywordColor: "rgba(255, 215, 0, 1)", // Gold for action backgroundColor: "rgba(0, 0, 0, 0.7)" }, scenes: [{ story: "Subscribe now and get exclusive access!", highlightKeywords: true }] } ``` **Result:** "Subscribe", "exclusive access" highlighted in gold. ### Mixed Highlighting Across Scenes ```javascript theme={null} { scenes: [ { story: "Introduction to our platform", highlightKeywords: false // No highlighting for intro }, { story: "Key features include automation and analytics", highlightKeywords: true // Highlight key features }, { story: "Closing thoughts and summary", hideSubtitles: true // No subtitles for outro } ] } ``` **Result:** Strategic highlighting only where it adds value. ## Best Practices Select keyword colors that stand out: * **High Contrast:** Ensure keyword color contrasts with both regular text and background * **Brand Colors:** Use brand colors for consistency * **Readability:** Test that highlighted text is still easy to read * **Common Choices:** Gold, light blue, bright red, or neon green * **Avoid Similar:** Don't use colors too similar to regular text color Apply keyword highlighting where it adds value: * **Educational Videos:** Highlight technical terms and concepts * **Marketing:** Emphasize product names and benefits * **Social Media:** Draw attention to calls-to-action * **Tutorials:** Highlight steps and important instructions * **Avoid Overuse:** Not every scene needs highlighting Structure your text to work well with keyword detection: * **Use Strong Nouns:** Include specific, meaningful nouns * **Clear Concepts:** State ideas explicitly rather than implying * **Product Names:** Capitalize proper nouns consistently * **Action Words:** Use clear verbs for calls-to-action * **Natural Language:** Write conversationally but with key terms Preview to ensure highlighting works as intended: * **Create Test Videos:** Try short samples first * **Check Keywords:** Verify important words are highlighted * **Color Visibility:** Ensure keyword color is visible * **Over-Highlighting:** Check if too many words are highlighted * **Adjust Content:** Rewrite if highlighting is not effective Enhance keyword highlighting with complementary features: * **Custom Styles:** Use bold or larger font for keyword emphasis * **Animations:** Add subtle entry animations to draw attention * **Voice Emphasis:** Pair with voice-over for audio+visual emphasis * **Scene Timing:** Give highlighted scenes slightly longer duration * **Background Music:** Lower music volume during key highlighted sections ## Troubleshooting **Problem:** Text appears but no words are highlighted in different color. **Solution:** * Ensure `highlightKeywords: true` is set in scene configuration * Verify `keywordColor` is defined in `subtitleStyle` * Check keyword color is different from regular text color * Confirm RGBA format is correct for keywordColor * Test with content that has clear important nouns/verbs * Try more explicit, keyword-rich text **Problem:** AI highlights unexpected or less important words. **Solution:** * Rewrite content to emphasize desired keywords * Use more specific, concrete nouns instead of generic words * Place important concepts at start or end of sentences * Capitalize proper nouns (product names, brands) * Remove filler words and keep text concise * Test with different phrasings to see what works best **Problem:** Highlighted keywords blend in or are hard to see. **Solution:** * Increase contrast between keywordColor and both text color and background * Try gold (`rgba(255, 215, 0, 1)`) for high visibility * Ensure alpha channel is 1.0 (fully opaque) for keyword color * Test on actual video backgrounds, not just specs * Use complementary colors (e.g., blue keywords on orange background) **Problem:** Nearly every word is highlighted, reducing impact. **Solution:** * Simplify your text - use fewer, more impactful words * The AI highlights based on importance; simpler text = clearer keywords * Remove redundant adjectives and adverbs * Focus on one key concept per sentence * Consider not using highlighting for that particular content **Problem:** Similar content highlighted differently in different scenes. **Solution:** * AI analyzes each scene independently - this is expected behavior * Ensure consistent phrasing for concepts you want highlighted * Use similar sentence structures across scenes * Capitalize important terms consistently * If needed, use same text multiple times for consistency **Problem:** Want subtitles on some scenes but not others. **Solution:** * Use `hideSubtitles: true` on scenes where you do not want subtitles * Combine with `highlightKeywords: false` for clarity * Useful for intro/outro scenes, transitions, or visual-only segments * Can also set different subtitle styles per scene * Test to ensure smooth visual transitions between scenes ## Next Steps Explore more subtitle and branding features: Create fully custom subtitle styles Use saved subtitle style presets Apply complete brand presets Add custom or translated captions ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Adding Logo to Videos Source: https://docs.pictory.ai/guides/branding-customization/logo Add your logo to videos with custom positioning and styling This guide shows you how to add your company logo or watermark to videos with full control over positioning and size. Perfect for branding videos without creating a full brand preset. ## What You'll Learn Add logos from URL to any video Place logo anywhere on screen Control logo width as a percentage of video PNG, JPG, SVG, and GIF logos ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Logo image file accessible via public URL * Basic understanding of image positioning ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Logo Placement Works When you add a logo to your video: 1. **Logo Access** - Your logo image is downloaded from the provided URL 2. **Position Calculation** - Logo is placed at the specified screen position 3. **Size Application** - Logo is sized based on the width percentage you specify 4. **Video Rendering** - Logo appears on all scenes throughout the video The logo appears on every scene in your video by default. For scene-specific logos, you can apply logo settings at the scene level instead of the video level. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media."; const LOGO_URL = "https://pictory-static.pictorycontent.com/logo.png"; async function createVideoWithLogo() { try { console.log("Creating video with logo..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_with_logo", // Logo configuration logo: { url: LOGO_URL, // Public URL to logo image position: "top-right", // Logo placement width: "15%", // 15% of video width }, // Scene configuration scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with logo is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithLogo(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media." LOGO_URL = "https://pictory-static.pictorycontent.com/logo.png" def create_video_with_logo(): try: print("Creating video with logo...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_with_logo', # Logo configuration 'logo': { 'url': LOGO_URL, # Public URL to logo image 'position': 'top-right', # Logo placement 'width': '15%' # 15% of video width }, # Scene configuration 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with logo is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_logo() ``` ## Understanding the Parameters ### Logo Configuration | Parameter | Type | Required | Description | | --------------- | ------ | -------- | ------------------------------------------------------- | | `logo.url` | string | Yes | Public URL to your logo image file | | `logo.position` | string | No | Logo placement on screen (default: `top-right`) | | `logo.width` | string | No | Logo width as percentage of video width (e.g., `"15%"`) | ## Logo Position Options Choose where to place your logo on the video: | Position | Location | Best Used For | | --------------- | ------------------ | -------------------------------- | | `top-left` | Upper left corner | Alternative watermark placement | | `top-center` | Upper center | Centered branding | | `top-right` | Upper right corner | Standard watermark (most common) | | `center-left` | Middle left | Side branding | | `center-center` | Center of frame | Intro/outro logos | | `center-right` | Middle right | Side branding | | `bottom-left` | Lower left corner | Legal/copyright notices | | `bottom-center` | Lower center | Centered footer branding | | `bottom-right` | Lower right corner | Alternative watermark | **Most Popular:** `top-right` is the standard position for watermarks and brand logos, as it is visible but does not interfere with subtitles (typically at bottom). ## Width (Size) Reference Control logo size relative to video width using a percentage string: | Width Value | Effect | | ----------- | ----------------------------- | | `"5%"` | Very small, minimal watermark | | `"10%"` | Small, subtle branding | | `"15%"` | Standard size (recommended) | | `"20%"` | Medium, noticeable branding | | `"25%"` | Large logo | | `"30%"` | Very large, dominant logo | **Avoid Extremes:** Logos smaller than `"5%"` may be hard to see, while logos larger than `"30%"` can be distracting and cover important content. ## Supported Image Formats | Format | Extension | Supports Transparency | Recommended For | | -------- | --------------- | --------------------- | -------------------------------------- | | PNG | `.png` | Yes | Logos with transparency (best choice) | | JPG/JPEG | `.jpg`, `.jpeg` | No | Photographic logos | | SVG | `.svg` | Yes | Vector logos, scalable graphics | | GIF | `.gif` | Yes | Static logos (animation not supported) | **Best Format:** Use PNG with transparent background for professional-looking logos that blend seamlessly with any video background. ## Common Use Cases ### Professional Watermark ```javascript theme={null} { logo: { url: "https://example.com/company-logo.png", position: "top-right", width: "15%" } } ``` **Result:** Subtle, professional watermark in the standard position. ### Prominent Branding ```javascript theme={null} { logo: { url: "https://example.com/brand-logo.png", position: "top-center", width: "25%" } } ``` **Result:** Large, visible logo for strong brand presence. ### Minimal Copyright Notice ```javascript theme={null} { logo: { url: "https://example.com/copyright-mark.png", position: "bottom-left", width: "8%" } } ``` **Result:** Small, subtle copyright or trademark symbol. ### Intro/Outro Logo ```javascript theme={null} { logo: { url: "https://example.com/large-logo.png", position: "center-center", width: "40%" } } ``` **Result:** Large centered logo for introduction or conclusion scenes. ## Best Practices Prepare your logo file for best results: * **PNG Format:** Use PNG with transparent background for clean edges * **High Resolution:** Minimum 512x512 pixels for clarity * **Simple Design:** Logos with less detail scale better * **Appropriate Colors:** Ensure logo is visible on various backgrounds * **Test First:** Preview logo on different content types before production Select logo placement strategically: * **Top-Right:** Standard for watermarks, does not conflict with subtitles * **Top-Left:** Alternative for different visual balance * **Avoid Center:** Center placement can distract from main content * **Consider Subtitles:** Do not overlap with caption placement (usually bottom) * **Test Visibility:** Ensure logo is visible on light and dark scenes Choose width based on video type: * **Standard Videos:** `"12%"` to `"18%"` works well for most content * **Social Media:** `"10%"` to `"15%"` for subtle branding * **Corporate Videos:** `"15%"` to `"20%"` for stronger presence * **Intro/Outro Only:** Consider scene-level logos instead * **Consistency:** Use same size across all videos for brand recognition Make your logo file accessible to the API: * **Cloud Storage:** Upload to Google Drive, Dropbox, AWS S3, or CDN * **Public URL:** Ensure the URL is publicly accessible (no authentication) * **Direct Link:** Use direct file URL, not preview or viewer links * **Test Access:** Open URL in incognito browser to verify * **HTTPS:** Use secure HTTPS URLs for best compatibility ## Troubleshooting **Problem:** Video is created but logo is missing. **Solution:** * Verify the logo URL is publicly accessible (test in incognito browser) * Check that the image file format is supported (PNG, JPG, SVG, GIF) * Confirm the URL is a direct link to the image file * Try a different image URL to test if it is a file-specific issue **Problem:** Logo quality is poor in the final video. **Solution:** * Use a higher resolution logo image (minimum 512x512 pixels) * Avoid scaling logo too large (keep width below `"30%"`) * Use PNG or SVG format instead of JPG for sharper edges * Check source image quality before using * For vector logos, use SVG format when possible **Problem:** Logo appears with a white or colored background instead of being transparent. **Solution:** * Use PNG format with transparent background * Re-export logo with alpha channel (transparency) enabled * Check original file has transparent background (open in image editor) * Avoid JPG format which does not support transparency * Use online tools to remove background if needed **Problem:** Logo size does not match expectations. **Solution:** * Adjust the `width` parameter (e.g., `"5%"` to `"30%"`) * Width is relative to video width, not height * Test different width values: `"10%"` (small), `"15%"` (medium), `"20%"` (large) * Remember larger videos will show logos larger at same width percentage * Preview before full production **Problem:** Logo does not appear where expected. **Solution:** * Verify position value matches available options * Position names are case-sensitive (e.g., "top-right" not "Top-Right") * Check for typos in position parameter * Logo is positioned based on its center point * Try different positions to find best placement **Problem:** Logo covers subtitles or key visuals. **Solution:** * Move logo to a different position (avoid bottom if using subtitles) * Reduce logo size (lower width value, e.g., `"10%"`) * Consider placing logo in corner with least content * Test with actual video content before production ## Next Steps Explore more branding and customization options: Use complete brand presets for full consistency Apply saved subtitle styling to videos Create custom subtitle styles inline Emphasize important words in subtitles ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Saved Subtitle Styles Source: https://docs.pictory.ai/guides/branding-customization/subtitle-styles Apply saved subtitle styles to maintain consistent caption formatting across videos This guide shows you how to apply saved subtitle styles to your videos for consistent caption formatting. You can use subtitle styles saved in your Pictory account at both video and scene levels. ## What You'll Learn Use pre-configured subtitle styles by name or ID Apply default styles to all scenes Customize specific scenes with different styles Choose between style name or ID ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Subtitle style created in your Pictory account * Subtitle style name or ID from your Pictory settings ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Subtitle Styles Work When you apply subtitle styles to your video: 1. **Style Selection** - You specify the style by name or ID 2. **Video-Level Application** - Style is set as default for all scenes 3. **Scene-Level Override** - Individual scenes can use different styles 4. **Automatic Formatting** - All subtitle properties are applied consistently 5. **Video Rendering** - Captions appear with your chosen formatting Subtitle styles must be created in your Pictory account before using them via the API. Scene-level styles override video-level styles for that specific scene. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media."; const STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency."; async function createVideoWithSubtitleStyles() { try { console.log("Creating video with subtitle styles..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_subtitle_style", // Video-level subtitle style (default for all scenes) subtitleStyleName: "{YOUR_STYLE_NAME}", // Use style name OR ID, not both scenes: [ { story: STORY_TEXT_1, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // This scene uses the video-level subtitle style }, { story: STORY_TEXT_2, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // Scene-level override with different style subtitleStyleName: "{DIFFERENT_STYLE_NAME}", }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with subtitle styles is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithSubtitleStyles(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' STORY_TEXT_1 = "AI is poised to significantly impact educators and course creators on social media." STORY_TEXT_2 = "By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency." def create_video_with_subtitle_styles(): try: print("Creating video with subtitle styles...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_subtitle_style', # Video-level subtitle style (default for all scenes) 'subtitleStyleName': '{YOUR_STYLE_NAME}', # Use style name OR ID, not both 'scenes': [ { 'story': STORY_TEXT_1, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False # This scene uses the video-level subtitle style }, { 'story': STORY_TEXT_2, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, # Scene-level override with different style 'subtitleStyleName': '{DIFFERENT_STYLE_NAME}' } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with subtitle styles is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_subtitle_styles() ``` ## Understanding the Parameters ### Video-Level Parameters | Parameter | Type | Required | Description | | ------------------- | ------ | -------- | -------------------------------------------------------------------- | | `subtitleStyleName` | string | No | Name of saved subtitle style (use this OR subtitleStyleId, not both) | | `subtitleStyleId` | string | No | ID of saved subtitle style (use this OR subtitleStyleName, not both) | ### Scene-Level Parameters | Parameter | Type | Required | Description | | ---------------------------- | ------ | -------- | ---------------------------------------------------------- | | `scenes[].subtitleStyleName` | string | No | Override video-level style for this scene using style name | | `scenes[].subtitleStyleId` | string | No | Override video-level style for this scene using style ID | **Cannot Use Both Together** You must choose either `subtitleStyleId` OR `subtitleStyleName` - never both at the same time: ❌ **WRONG:** ```json theme={null} { "subtitleStyleId": "8f3deae9-fe38-4c53-8be0-616bef1da916", "subtitleStyleName": "Navy blue" } ``` ✅ **CORRECT (choose one):** ```json theme={null} { "subtitleStyleId": "8f3deae9-fe38-4c53-8be0-616bef1da916" } ``` OR ```json theme={null} { "subtitleStyleName": "Navy blue" } ``` **Additional Rules:** * This mutual exclusivity applies at both video level and scene level * Scene-level styles completely override video-level styles * The style must exist in your Pictory account * Style names and IDs are account-specific ## Style Name vs Style ID Choose the right method for your use case: | Use Case | Recommendation | Why | | ------------------------ | ----------------------- | -------------------------------------------- | | Human-readable code | Use `subtitleStyleName` | Easier to read and maintain | | Programmatic references | Use `subtitleStyleId` | Guaranteed stability if style is renamed | | Style names might change | Use `subtitleStyleId` | IDs remain constant even if name changes | | Easier debugging | Use `subtitleStyleName` | Clear what style you are using from the name | | API automation | Use `subtitleStyleId` | More reliable for automated systems | ## What Subtitle Styles Include Subtitle styles configure all caption formatting elements: | Element | What It Controls | | --------------- | ----------------------------------------------------- | | **Font** | Font family, size, and weight | | **Colors** | Text color, background color, keyword highlight color | | **Position** | Vertical and horizontal placement on screen | | **Alignment** | Left, center, or right text alignment | | **Decorations** | Bold, italic, underline, strikethrough | | **Effects** | Shadow, outline, and glow settings | | **Animations** | Entry and exit animation effects | | **Layout** | Paragraph width and spacing | ## Finding Your Subtitle Styles ### Option 1: Using the Pictory Web Interface 1. Log in to your Pictory account 2. Navigate to **Subtitle Styles** or **Settings** 3. View your saved subtitle styles 4. Note the style name or ID for API use ### Option 2: Using the API Retrieve all your saved subtitle styles programmatically: ```javascript Node.js theme={null} const stylesResponse = await axios.get( `${API_BASE_URL}/v1/styles`, { headers: { Authorization: API_KEY } } ); console.log('Your subtitle styles:'); stylesResponse.data.forEach(style => { console.log(`- Name: ${style.name}`); console.log(` ID: ${style.id}`); }); // Use either the 'name' or 'id' field ``` ```python Python theme={null} styles_response = requests.get( f'{API_BASE_URL}/v1/styles', headers={'Authorization': API_KEY} ) print('Your subtitle styles:') for style in styles_response.json(): print(f"- Name: {style['name']}") print(f" ID: {style['id']}") # Use either the 'name' or 'id' field ``` See [Get Text Styles](/api-reference/branding/get-text-styles) for complete API documentation. ## Common Use Cases ### Consistent Branding Across Videos ```javascript theme={null} { videoName: "company_announcement", subtitleStyleName: "Corporate Blue" } ``` **Result:** All videos use the same professional subtitle styling. ### Different Styles for Different Scenes ```javascript theme={null} { subtitleStyleName: "Standard Style", // Default for most scenes scenes: [ { story: "Introduction text", // Uses default "Standard Style" }, { story: "Important announcement", subtitleStyleName: "Bold Emphasis" // Override for this scene }, { story: "Conclusion text", // Back to default "Standard Style" } ] } ``` **Result:** Most scenes use standard styling, one scene uses bold emphasis for importance. ### Style by ID for Stability ```javascript theme={null} { subtitleStyleId: "8f3deae9-fe38-4c53-8be0-616bef1da916" } ``` **Result:** Style applied by ID remains correct even if style is renamed. ## Best Practices Build a library of subtitle styles for different use cases: * **Corporate:** Professional styles for business content * **Social Media:** Casual, eye-catching styles for social platforms * **Educational:** Clear, readable styles for learning content * **Marketing:** Bold, attention-grabbing styles for promotions * **Accessibility:** High-contrast styles for better readability This allows quick style selection based on video type. Name your subtitle styles clearly: * **Good:** "Bold White on Black", "Casual Yellow Highlight" * **Avoid:** "Style1", "New", "Test" Clear names make it easier to select the right style and maintain your code. Before using a subtitle style via API: * Create a test video in the Pictory web interface * Verify the style looks as expected * Check readability on different backgrounds * Test on various video types * Make adjustments in Pictory settings if needed Decide on a strategy and stick with it: * **Style Names:** If you rarely rename styles and want readable code * **Style IDs:** If styles might be renamed or for automated systems * **Document Choice:** Note in your codebase which method you are using * **Team Alignment:** Ensure whole team uses same approach Apply scene-specific styles strategically: * **Emphasis:** Use different style for important messages * **Sections:** Different styles for intro, main content, outro * **Consistency:** Do not change styles too frequently * **Purpose:** Each style change should have clear intent * **Testing:** Preview videos to ensure style changes feel natural ## Troubleshooting **Problem:** API returns error that subtitle style does not exist. **Solution:** * Verify style name is spelled exactly as saved in Pictory * Check for case sensitivity (e.g., "Bold" vs "bold") * Use Get Text Styles API to see all available styles * Ensure the style exists in your account * Try using style ID instead of name **Problem:** Captions appear with different formatting than expected. **Solution:** * Double-check the style name or ID in your request * Ensure you are not using both name and ID (use only one) * Verify the style hasn't been modified in Pictory * Check for scene-level overrides that might be applying * Review completed video to confirm issue **Problem:** Scene uses video-level style instead of scene-level override. **Solution:** * Ensure scene-level style parameter is inside the scene object * Check JSON syntax is correct (commas, brackets, quotes) * Verify scene-level style name or ID is valid * Make sure you are not mixing name and ID parameters * Test with a simple example first **Problem:** Unsure whether to use subtitleStyleName or subtitleStyleId. **Solution:** * **Use Name if:** You want readable code and rarely rename styles * **Use ID if:** Styles might be renamed or you need guaranteed stability * **Use ID if:** Building automated systems with programmatic references * **Use Name if:** Working on small projects with few styles * Either works - choose based on your specific needs **Problem:** Subtitle styling via API does not match Pictory interface. **Solution:** * Confirm you are using the correct style name or ID * Check that the style hasn't been modified since testing * Ensure no other parameters are overriding style settings * Use Get Text Styles API to verify style configuration * Test the same style in Pictory UI to compare ## Next Steps Explore more subtitle customization options: Create inline styles without saving presets Emphasize important words automatically Use complete brand presets including styles Add custom caption text separate from story ## API Reference For complete technical details, see: * [Get Text Styles](/api-reference/branding/get-text-styles) - List all available subtitle styles * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Dynamic Captions Source: https://docs.pictory.ai/guides/captions/dynamic-captions Add dynamic captions to AI-generated videos using the Pictory API. Control caption lines with maxSubtitleLines and sync with AI voiceover. Dynamic captions are progressive on-screen subtitles controlled by the `maxSubtitleLines` parameter. This parameter determines how many lines of caption text appear on screen at any given time. When AI voiceover is enabled, each caption line is synchronized with the spoken audio, appearing precisely as it is narrated. ## What You Will Learn Configure caption lines that appear progressively on screen Use maxSubtitleLines to set the number of visible caption lines Compatible with ElevenLabs, AWS Polly, and Google voices ## Prerequisites Ensure you have the following before proceeding: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Text content ready for video conversion ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Dynamic Captions Work The `maxSubtitleLines` parameter enables dynamic captions at the scene level. When included in a scene configuration, Pictory generates caption lines that appear progressively on screen, limited to the specified number of lines. ### With AI Voiceover When AI voiceover is enabled alongside `maxSubtitleLines`, caption lines are synchronized with the spoken audio: 1. **Audio Generation:** The story text is converted to speech using the selected AI voice. 2. **Line Timing:** The audio service provides timing data that determines when each caption line appears. 3. **Caption Sync:** Each caption line is displayed on screen at the exact moment it is spoken in the voiceover. 4. **Line Control:** The `maxSubtitleLines` value limits how many lines of text are visible simultaneously. ### Without AI Voiceover The `maxSubtitleLines` parameter can also be used without voiceover. In this mode, caption lines are distributed uniformly across the scene duration, with each line displayed for an equal amount of time. When `maxSubtitleLines` is not provided, all caption lines for the scene are displayed on screen at once. To enable progressive caption display, set `maxSubtitleLines` to the desired number of visible lines. For the best results, pair `maxSubtitleLines` with AI voiceover to synchronize caption lines with the spoken audio. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithDynamicCaptions() { try { console.log("Creating video with dynamic captions..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "dynamic_captions_demo", // Voice-over enables caption sync with spoken audio voiceOver: { enabled: true, aiVoices: [ { speaker: "Rachel", speed: 100, amplificationLevel: 0, }, ], }, // Scenes with dynamic captions scenes: [ { story: "Creating professional videos used to take hours of editing, expensive software, and a dedicated production team. With Pictory, you can turn any script into a stunning video in minutes, powered by artificial intelligence.", maxSubtitleLines: 2, // Display 2 lines of captions at a time }, { story: "Our dynamic captions feature automatically syncs with your voiceover, making every video accessible, engaging, and optimized for social media. No manual timing, no tedious adjustments.", maxSubtitleLines: 2, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with dynamic captions is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error( "Video creation failed: " + JSON.stringify(statusResponse.data) ); } await new Promise((resolve) => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithDynamicCaptions(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_dynamic_captions(): try: print("Creating video with dynamic captions...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'dynamic_captions_demo', # Voice-over enables caption sync with spoken audio 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Rachel', 'speed': 100, 'amplificationLevel': 0 } ] }, # Scenes with dynamic captions 'scenes': [ { 'story': 'Creating professional videos used to take hours of editing, expensive software, and a dedicated production team. With Pictory, you can turn any script into a stunning video in minutes, powered by artificial intelligence.', 'maxSubtitleLines': 2 # Display 2 lines of captions at a time }, { 'story': 'Our dynamic captions feature automatically syncs with your voiceover, making every video accessible, engaging, and optimized for social media. No manual timing, no tedious adjustments.', 'maxSubtitleLines': 2 } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with dynamic captions is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_dynamic_captions() ``` ## Understanding the Parameters ### Scene-Level Configuration | Parameter | Type | Required | Description | | --------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------- | | `scenes[].maxSubtitleLines` | number | No | Maximum number of caption lines displayed simultaneously. When omitted, all lines are shown on the scene at once. | ### maxSubtitleLines Values | Value | Description | | ------- | ---------------------------------------------------------------------------------- | | Not set | All caption lines are displayed on the scene at once (no progressive display) | | `1` | Displays one caption line at a time. Ideal for short-form content | | `2` | Standard caption display. Suitable for most content types | | `3` | Displays more text simultaneously. Recommended for educational or detailed content | | `4` | Maximum text density. Best for information-heavy content | When `maxSubtitleLines` is not included in the request, all caption text for the scene is shown at once. Setting this parameter activates progressive caption display. Enabling AI voiceover synchronizes caption lines with the spoken audio. ## Line Count by Use Case ### Single Line (maxSubtitleLines: 1) Suited for short-form social media content where concise, fast-paced text is preferred: ```javascript theme={null} { scenes: [{ story: "Your content here...", maxSubtitleLines: 1 }] } ``` **Result:** Clean, single-line captions optimized for quick readability. Ideal for TikTok, Instagram Reels, and YouTube Shorts. ### Two Lines (maxSubtitleLines: 2) The most common configuration, providing a balance between readability and content density: ```javascript theme={null} { scenes: [{ story: "Your content here...", maxSubtitleLines: 2 }] } ``` **Result:** Standard two-line captions suitable for most video formats. Recommended for YouTube, LinkedIn, and marketing videos. ### Three Lines (maxSubtitleLines: 3) Appropriate for longer sentences or content that benefits from additional on-screen context: ```javascript theme={null} { scenes: [{ story: "Your detailed content with longer sentences...", maxSubtitleLines: 3 }] } ``` **Result:** Increased text visibility per frame. Ideal for educational, documentary, or training content. ## Mixed Line Counts Different `maxSubtitleLines` values can be assigned to individual scenes within the same video: ```javascript theme={null} { scenes: [ { story: "Quick intro text for social media hook.", maxSubtitleLines: 1 // Single line for the opening hook }, { story: "Now let me explain in more detail how this technology works and why it matters for your business growth strategy.", maxSubtitleLines: 3 // Additional lines for the detailed explanation } ] } ``` ## Aspect Ratio Considerations The optimal `maxSubtitleLines` value depends on the video aspect ratio: | Aspect Ratio | Orientation | Recommendation | | ------------ | ----------- | -------------- | | `16:9` | Landscape | 2-3 lines | | `1:1` | Square | 2 lines | | `4:5` | Vertical | 1-2 lines | | `9:16` | Portrait | 1-2 lines | Vertical and portrait formats (4:5 and 9:16) have limited horizontal space, causing longer sentences to wrap more frequently. Use fewer lines for vertical content to maintain readability. ## Supported Voice Providers Dynamic captions with voiceover synchronization are supported across all voice providers: | Provider | Example Voices | Caption Sync Method | | ---------- | -------------- | ------------------------- | | ElevenLabs | Rachel, Adam | Native timing alignment | | AWS Polly | Aria, Matthew | Via transcription service | | Google TTS | Eugene, Lisa | Via transcription service | **Compatibility** Dynamic captions via `maxSubtitleLines` function with or without AI voiceover. When AI voiceover is enabled, caption lines are synchronized with the spoken audio, appearing on screen as each line is narrated. **Supported Workflows:** * Text-to-Video (using `story` parameter) * Article-to-Video (using `blogUrl` parameter) * Image slideshows with text overlays **Not Supported With:** * `smartLayoutName` or `smartLayoutId` (smart layouts manage text display independently) * The `caption` parameter (uses separate text handling) * Audio-to-Video workflows (`audioUrl`) * Video-to-Video workflows (`videoUrl`) * PowerPoint-to-Video workflows (`pptUrl`) ## Best Practices Select the line count based on the target publishing platform: * **TikTok/Reels:** 1 line for rapid readability * **YouTube:** 2 lines for standard viewing * **Educational platforms:** 2-3 lines for detailed content * **Presentations:** 2 lines for a professional appearance While dynamic captions function without voiceover, enabling AI voiceover synchronizes caption lines with the spoken audio, providing the best viewer experience: ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Rachel" }] } } ``` Account for viewer reading speed when selecting a line count: * Fewer lines result in faster comprehension * More lines provide additional context but require more processing time * Match line count to content complexity Line count and font size are interdependent: * Larger fonts require fewer lines to avoid overcrowding * Smaller fonts can accommodate additional lines * Verify that text remains legible at all sizes * Test across different screen dimensions When `maxSubtitleLines` is not provided, all caption text for the scene is displayed at once. To enable progressive caption display, explicitly set this parameter to the desired line count. ## Troubleshooting **Cause:** Caption lines appear on screen but are not synchronized with the voiceover. **Resolution:** 1. Verify that voice-over is enabled in the request 2. Confirm that `maxSubtitleLines` is set at the scene level 3. Ensure that smart layouts are not in use, as they manage text display independently **Cause:** The API returns an error because `maxSubtitleLines` cannot be combined with smart layouts. **Resolution:** 1. Remove `maxSubtitleLines` from the scene configuration, or 2. Remove `smartLayoutName` or `smartLayoutId` if dynamic captions are required Smart layouts and dynamic captions are mutually exclusive. Select one approach per scene. **Cause:** The API returns an error because `maxSubtitleLines` and `caption` cannot be used together. **Resolution:** 1. Remove either the `maxSubtitleLines` or `caption` parameter 2. Use the `story` parameter with `maxSubtitleLines` for dynamic captions The `caption` parameter uses its own text handling and is incompatible with dynamic captions. **Cause:** Subtitles appear with more lines than the configured `maxSubtitleLines` value. **Resolution:** 1. Verify the parameter is spelled correctly: `maxSubtitleLines` 2. Confirm the parameter is set at the scene level, not the video level 3. Ensure that smart layouts are not active on the scene 4. Reduce font size if long words are causing unexpected line wrapping ## Next Steps Enhance your dynamic captions with these related features: Customize font, color, and background of your captions Apply saved subtitle style presets Emphasize important words in captions Use pre-designed layouts with automatic text handling ## API Reference For complete technical details, refer to the following resources: Configure dynamic captions and voice-over settings Monitor video creation progress Retrieve available subtitle styles Retrieve all available smart layout templates # Text Animations & Future Words Source: https://docs.pictory.ai/guides/captions/text-animations Animate captions with entry and exit effects and control upcoming word visibility using the futureWords parameter in the Pictory API. Text animations add entry and exit effects to your video captions, enhancing visual engagement. The `futureWords` parameter, available with `fade` and `blur` animations, controls how upcoming words appear on screen before they are spoken. This combination is particularly effective for creating engaging, word-synchronized captions for social media content. ## What You Will Learn Apply entry and exit animation effects to scene captions Control the visibility of upcoming unspoken words on screen Select from fade, blur, drift, wipe, text reveal, elastic, and typewriter effects Produce scroll-stopping content for TikTok, Reels, and Shorts ## Prerequisites Ensure you have the following before proceeding: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Familiarity with [Dynamic Captions](/guides/captions/dynamic-captions) fundamentals * AI voiceover enabled for word-level caption synchronization with audio ## Available Animations ### Entry Animations | Animation | `futureWords` Support | Direction | Description | | ------------- | --------------------- | ----------------------------- | -------------------------------------------- | | `fade` | Yes | - | Words fade in smoothly as they are spoken | | `blur` | Yes | - | Words transition from blurred to sharp focus | | `drift` | No | `up`, `down`, `left`, `right` | Words slide in from the specified direction | | `wipe` | No | `left`, `right`, `up`, `down` | Words are revealed with a sweeping motion | | `text reveal` | No | - | Words uncover progressively | | `elastic` | No | - | Words bounce in with a spring effect | | `typewriter` | No | - | Words appear one letter at a time | ### Exit Animations | Animation | Direction | Description | | ------------- | ----------------------------- | ------------------------------------------ | | `fade` | - | Words fade out smoothly | | `blur` | - | Words blur out of focus | | `drift` | `up`, `down`, `left`, `right` | Words slide out in the specified direction | | `wipe` | `left`, `right`, `up`, `down` | Words are swept away | | `text reveal` | - | Words are progressively hidden | `elastic` and `typewriter` are entry-only animations. They do not have exit variants. ## The futureWords Parameter The `futureWords` parameter controls the on-screen appearance of words that have not yet been spoken. This parameter is available exclusively with `fade` and `blur` entry animations. | Value | Appearance | Effect | Recommended For | | ------------- | --------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------- | | `"hidden"` | Upcoming words are invisible | Words appear only when spoken, creating a maximum reveal effect | TikTok, Instagram Reels, high-energy content | | `"subtle"` | Upcoming words are faintly visible | Viewers can preview upcoming text while attention remains on the current word | YouTube Shorts, educational content | | `"prominent"` | Upcoming words are clearly visible but dimmed | Full text is readable with the spoken word highlighted | Professional videos, presentations | ### Visual Comparison **`futureWords: "hidden"`** (words revealed as spoken) ``` Timeline: "The future of AI is here" t=0.0s: [The] ______ __ __ __ ____ (only "The" visible) t=0.3s: The [future] __ __ __ ____ ("future" appears as spoken) t=0.7s: The future [of] __ __ ____ (words reveal one by one) t=0.9s: The future of [AI] __ ____ t=1.1s: The future of AI [is] ____ t=1.3s: The future of AI is [here] ``` **`futureWords: "subtle"`** (soft preview of upcoming text) ``` Timeline: "The future of AI is here" t=0.0s: [The] future of AI is here ("The" bright, rest faint) t=0.3s: The [future] of AI is here ("future" brightens) t=0.7s: The future [of] AI is here (spoken words remain bright) ``` **`futureWords: "prominent"`** (full text with active word highlighted) ``` Timeline: "The future of AI is here" t=0.0s: [The] future of AI is here ("The" highlighted, rest dimmed) t=0.3s: The [future] of AI is here ("future" now highlighted) t=0.7s: The future [of] AI is here (highlight follows the voiceover) ``` ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createAnimatedCaptionsVideo() { try { console.log("Creating video with animated captions..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "animated_captions_demo", videoWidth: 1080, videoHeight: 1920, quality: "high", voiceOver: { enabled: true, aiVoices: [{ speaker: "Rachel", speed: 100 }], }, scenes: [ { story: "Five tips to grow your social media following this year. Number one, post consistently at least three times per week.", maxSubtitleLines: 1, subtitleStyle: { animations: [ { name: "fade", type: "entry", speed: "fast", futureWords: "hidden", }, { name: "fade", type: "exit", speed: "fast" }, ], }, }, { story: "Number two, engage with your audience by replying to every comment within the first hour of posting.", maxSubtitleLines: 1, subtitleStyle: { animations: [ { name: "blur", type: "entry", speed: "medium", futureWords: "subtle", }, { name: "blur", type: "exit", speed: "fast" }, ], }, }, { story: "Number three, use trending audio and hashtags to reach new audiences organically.", maxSubtitleLines: 2, subtitleStyle: { animations: [ { name: "fade", type: "entry", speed: "medium", futureWords: "prominent", }, { name: "fade", type: "exit", speed: "medium" }, ], }, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video creation started! Job ID:", jobId); } catch (error) { console.error("Error:", error.response?.data || error.message); } } createAnimatedCaptionsVideo(); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_animated_captions_video(): try: print("Creating video with animated captions...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'animated_captions_demo', 'videoWidth': 1080, 'videoHeight': 1920, 'quality': 'high', 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Rachel', 'speed': 100}] }, 'scenes': [ { 'story': 'Five tips to grow your social media following this year. Number one, post consistently at least three times per week.', 'maxSubtitleLines': 1, 'subtitleStyle': { 'animations': [ { 'name': 'fade', 'type': 'entry', 'speed': 'fast', 'futureWords': 'hidden' }, {'name': 'fade', 'type': 'exit', 'speed': 'fast'} ] } }, { 'story': 'Number two, engage with your audience by replying to every comment within the first hour of posting.', 'maxSubtitleLines': 1, 'subtitleStyle': { 'animations': [ { 'name': 'blur', 'type': 'entry', 'speed': 'medium', 'futureWords': 'subtle' }, {'name': 'blur', 'type': 'exit', 'speed': 'fast'} ] } }, { 'story': 'Number three, use trending audio and hashtags to reach new audiences organically.', 'maxSubtitleLines': 2, 'subtitleStyle': { 'animations': [ { 'name': 'fade', 'type': 'entry', 'speed': 'medium', 'futureWords': 'prominent' }, {'name': 'fade', 'type': 'exit', 'speed': 'medium'} ] } } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Video creation started! Job ID: {job_id}") except requests.exceptions.RequestException as error: print(f"Error: {error}") if __name__ == '__main__': create_animated_captions_video() ``` ## Animation Examples ### Fade with Hidden Future Words Words appear only when spoken. Ideal for short-form social media content: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "fade", type: "entry", speed: "fast", futureWords: "hidden" }, { name: "fade", type: "exit", speed: "fast" } ] } } ``` ### Blur with Subtle Future Words Words transition from blurred to sharp focus. Upcoming words are faintly visible: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "blur", type: "entry", speed: "medium", futureWords: "subtle" }, { name: "blur", type: "exit", speed: "fast" } ] } } ``` ### Drift Animation Words slide in from a specified direction: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "drift", type: "entry", speed: "medium", direction: "up" }, { name: "drift", type: "exit", speed: "medium", direction: "down" } ] } } ``` ### Wipe Animation Words are revealed with a sweeping motion: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "wipe", type: "entry", speed: "medium", direction: "left" }, { name: "wipe", type: "exit", speed: "medium", direction: "right" } ] } } ``` ### Typewriter Animation Words appear one letter at a time: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "typewriter", type: "entry", speed: "fast" } ] } } ``` ### Elastic Animation Words bounce in with a spring effect: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "elastic", type: "entry", speed: "medium" } ] } } ``` ### Text Reveal Animation Words uncover progressively: ```javascript theme={null} { subtitleStyle: { animations: [ { name: "text reveal", type: "entry", speed: "medium" }, { name: "text reveal", type: "exit", speed: "fast" } ] } } ``` ## Platform Recommendations | Platform | Aspect Ratio | Recommended Animation | futureWords | maxSubtitleLines | | -------------- | ------------ | --------------------- | ------------- | ---------------- | | TikTok / Reels | 9:16 | `fade` | `"hidden"` | 1 | | YouTube Shorts | 9:16 | `blur` | `"subtle"` | 1-2 | | YouTube | 16:9 | `fade` | `"prominent"` | 2 | | LinkedIn | 16:9 | `fade` | `"prominent"` | 2 | | Instagram Feed | 4:5 | `blur` | `"subtle"` | 2 | ## Animation Parameters Reference | Parameter | Type | Required | Values | Description | | ------------------ | ------ | ---------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `name` | string | Yes | `"fade"`, `"blur"`, `"drift"`, `"wipe"`, `"text reveal"`, `"elastic"`, `"typewriter"` | The animation type to apply | | `type` | string | Yes | `"entry"`, `"exit"` | Specifies when the animation plays | | `speed` | string | Yes | `"slow"`, `"medium"`, `"fast"`, `"custom"` | The animation playback speed | | `customSpeedValue` | number | When speed is `"custom"` | Minimum 0.5 | Custom speed multiplier | | `futureWords` | string | No | `"hidden"`, `"subtle"`, `"prominent"` | Controls upcoming word visibility. Available only with `fade` and `blur` entry animations | | `direction` | string | When using `drift` or `wipe` | `"up"`, `"down"`, `"left"`, `"right"` | The direction of the animation movement | **Constraints:** * The `futureWords` parameter is available only with `fade` and `blur` entry animations * Each scene supports a maximum of 2 animations (1 entry + 1 exit) * `elastic` and `typewriter` are entry-only animations with no exit variant * When AI voiceover is enabled, `futureWords` synchronizes word highlighting with the spoken audio * The `futureWords` parameter must be specified on the `entry` animation, not the `exit` animation ## Best Practices * **TikTok / Reels:** `"hidden"` for maximum engagement through word reveal * **YouTube Shorts:** `"subtle"` to provide a preview of upcoming words * **YouTube / LinkedIn:** `"prominent"` for professional, fully readable captions * **Instagram Feed:** `"subtle"` for a balance between engagement and readability Select an animation speed that complements the voiceover delivery: * `"fast"` for quick-paced speakers and short-form content * `"medium"` for a balanced pace suitable for most content * `"slow"` for dramatic pauses and emphasis * `"custom"` with `customSpeedValue` for precise control For the most effective animated caption experience: * `maxSubtitleLines: 1` delivers the strongest impact for short-form content * `maxSubtitleLines: 2` provides a good balance for most platforms * Higher values display more text simultaneously, which can reduce the visual impact of word-level animations Different animations can be applied to individual scenes for visual variety: ```javascript theme={null} scenes: [ { story: "...", subtitleStyle: { animations: [{ name: "fade", ... }] } }, { story: "...", subtitleStyle: { animations: [{ name: "blur", ... }] } }, { story: "...", subtitleStyle: { animations: [{ name: "drift", ... }] } } ] ``` ## Troubleshooting **Cause:** Captions appear but words do not highlight at the correct time relative to the voiceover. **Resolution:** 1. Verify that `futureWords` is set on the entry animation, not the exit animation 2. Confirm that `type` is set to `"entry"` on the animation containing `futureWords` 3. Ensure AI voiceover is enabled for automatic word-level timing synchronization 4. Verify that the animation `name` is either `"fade"` or `"blur"`, as these are the only types that support `futureWords` **Cause:** The API returns an error when `futureWords` is included in the request. **Resolution:** 1. Confirm the animation `name` is `"fade"` or `"blur"`. Other animation types do not support `futureWords` 2. Remove `futureWords` from any `drift`, `wipe`, `text reveal`, `elastic`, or `typewriter` animation configurations **Cause:** Setting `futureWords: "hidden"` does not produce the expected word reveal behavior. **Resolution:** 1. Confirm that `futureWords` is specified on the `entry` animation, not the `exit` animation 2. Verify that `maxSubtitleLines` is set at the scene level 3. Enable AI voiceover to synchronize word appearance with the spoken audio ## Next Steps Learn the fundamentals of dynamic captions and the maxSubtitleLines parameter Customize font, color, and background of your captions Emphasize important words within captions Configure AI voices for your videos # Use Pictory with an LLM Source: https://docs.pictory.ai/guides/llm-integration/use-pictory-with-llm Guide an LLM end-to-end through the Pictory API using llms.txt, the MCP server, OpenAPI, and proven system prompts This guide shows you how to feed Pictory documentation to a Large Language Model (LLM) so the model can guide a user end-to-end — from describing a video they want, through building the right API payload, to retrieving the rendered video. By the end of this page, you will have: * A single URL you can paste into any LLM to make it Pictory-aware * A system prompt that turns the model into a Pictory API assistant * A reference list of example user prompts and the API calls they should produce * A working setup for the Pictory MCP server so agentic tools can call the API directly ## Why This Matters The Pictory API exposes many endpoints, dozens of optional fields, and several content-source types (text, blog URLs, PowerPoint, audio, video). A user describing their goal in natural language ("create a 60-second product demo with a chef avatar") needs **a lot of context** to be translated correctly into a request payload. LLMs handle this translation well — when given the right context. The pieces below give an LLM that context. *** ## 1. Point an LLM at `llms.txt` or `llms-full.txt` Pictory's documentation site automatically generates two machine-friendly bundles you can feed to any LLM: | File | URL | What It Contains | When to Use | | --------------- | --------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------- | | `llms.txt` | `https://docs.pictory.ai/llms.txt` | Index of all docs pages with titles and one-line descriptions | Quick navigation, smaller context windows | | `llms-full.txt` | `https://docs.pictory.ai/llms-full.txt` | **Entire documentation site concatenated into one Markdown file** | Maximum accuracy, large context windows | ### How to Use **Option A — Paste into a chat session.** Open any LLM chat (Claude, ChatGPT, Perplexity, Gemini) and paste the URL or the contents of `llms-full.txt` at the top of the conversation. Then ask your question normally. **Option B — Use the per-page Copy/Open buttons.** Every page on `docs.pictory.ai` has a "Copy" menu in the top-right with these options: **Copy as Markdown**, **Open in ChatGPT**, **Open in Claude**, **Open in Perplexity**, **Open in MCP**, **Open in Cursor**, **Open in VSCode**. Use these when you need context from a single page. **Option C — Reference the URL programmatically.** If you are building an agent or chatbot, fetch `llms-full.txt` once per session and pass it as a system prompt or context block: ```javascript Node.js theme={null} const docsContext = await fetch("https://docs.pictory.ai/llms-full.txt") .then((r) => r.text()); const systemPrompt = `${PICTORY_SYSTEM_PROMPT}\n\n## Pictory Docs\n\n${docsContext}`; ``` ```python Python theme={null} import requests docs_context = requests.get("https://docs.pictory.ai/llms-full.txt").text system_prompt = f"{PICTORY_SYSTEM_PROMPT}\n\n## Pictory Docs\n\n{docs_context}" ``` `llms-full.txt` is large. If your model's context window is small, prefer `llms.txt` and use the per-page Markdown URLs for the specific pages the user's question touches. *** ## 2. Use the Pictory OpenAPI Spec For deterministic codegen and structured tool use, the Pictory API publishes an OpenAPI 3.1 specification: `https://docs.pictory.ai/openapi.json` You can: * Feed this directly to an LLM as a structured tool schema * Generate client SDKs in any language (TypeScript, Python, Go, Ruby, etc.) using `openapi-generator` or `openapi-typescript` * Import into Postman, Insomnia, or any API client for interactive testing When you give an LLM both the OpenAPI spec and `llms-full.txt`, you get the best of both worlds — the structured field-level validation from OpenAPI, plus the narrative guidance and examples from the docs. *** ## 3. Recommended System Prompt Paste this as the system prompt (or the first message) in any LLM you want to act as a Pictory API assistant: ```markdown theme={null} You are a Pictory API assistant. You help users create videos programmatically using the Pictory API. ## Core Knowledge - **Authentication:** The Pictory API uses a direct API key in the `Authorization` header. The key starts with `pictai_`. Do NOT use a `Bearer` prefix. The header value is the raw key. - **Base URL:** `https://api.pictory.ai/pictoryapis` - **Primary endpoints:** - `POST /v2/video/storyboard` — Create a storyboard preview (review before rendering) - `POST /v2/video/storyboard/render` — Render the final video directly - `POST /v2/projects/{projectid}/render` — Re-render an existing project - `GET /v1/jobs/{jobid}` — Fetch the status and output of any job - `GET /v1/brands/video` — List the user's video brand kits ## How to Help 1. Ask the user what they want to create (text-to-video, PPT-to-video, avatar video, template-based, etc.). 2. Identify the right endpoint based on the input source. 3. Build a complete, valid request payload — never use placeholder values for required fields without flagging them. 4. Surface mutually exclusive fields (e.g., `brandId` vs `brandName`, `smartLayoutId` vs `smartLayoutName`, `subtitleStyleId` vs `subtitleStyleName`). 5. Always show how to poll the job status using `GET /v1/jobs/{jobid}` afterward, OR recommend the `webhook` field for async notification. 6. Include language hints when relevant. Supported `language` values: `zh, nl, en, fr, de, hi, it, ja, ko, mr, pt, ru, es, ta`. ## Dynamic IDs — Never Invent These The following fields take **account-scoped IDs that must be discovered at runtime**. Do NOT hallucinate values; do NOT memorize examples from training data. Call the discovery endpoint first, then use a real ID from the response. | Field | Discovery endpoint | Notes | |---|---|---| | `avatar.avatarId` | `GET /v1/avatars` | Lists AI avatars available to the user's account | | `brandId` / `brandName` | `GET /v1/brands/video` | Lists the user's saved brand kits | | `templateId` | `GET /v2/projects` or `GET /v1/templates` | Either an existing project ID (used as a template) or a saved template ID | | `smartLayoutId` / `smartLayoutName` | `GET /v1/smartlayouts` | Lists available smart layouts | | `subtitleStyleId` / `subtitleStyleName` | `GET /v1/styles` | Lists saved subtitle styles | | `voiceOver.aiVoices[].speaker` | `GET /v1/voiceovers/tracks` | Lists available AI voices | **Workflow:** when a user says "use my chef avatar", do not guess an ID. Either ask them for the ID explicitly, or instruct them to run `GET /v1/avatars` and pick from the response. The same applies to brands, templates, layouts, subtitle styles, and voices. ### Default Voice by Language When the user does not specify a voice but does specify a language, use the documented default male STD voice for that language. For languages without a documented default, the API falls back to the English default; in that case it is often better to call `GET /v1/voiceovers/tracks` and pick a native-language voice explicitly. | `language` | Default male STD voice | Notes | |---|---|---| | `en` | `Martin` | | | `nl` | `Tim` | Dutch | | `fr` | `Gabriel` | French | | `de` | `Wilbur` | German | | `it` | `Marco` | Italian | | `pt` | `Aurelio` | Portuguese | | `es` | `Hugo` | Spanish | | `hi` | `Martin` | Hindi — falls back to English | | `ru` | `Martin` | Russian — falls back to English | | `zh`, `ja`, `ko`, `mr`, `ta` | (none documented) | Discover a native voice via `GET /v1/voiceovers/tracks` | This table covers the documented STD male defaults. For female voices or non-default catalogs, call the voiceovers tracks endpoint and let the user choose. ## Output Format When generating an API call, always output: 1. A short summary of what the call will do (one sentence). 2. The complete cURL command (with `YOUR_API_KEY` as the placeholder). 3. A note about the response shape and how to retrieve the rendered video. ## Common Pitfalls to Avoid - Do not invent endpoints. If you are unsure, ask the user to confirm or reference the docs. - Do not invent or hallucinate IDs (`avatarId`, `brandId`, `templateId`, `smartLayoutId`, `subtitleStyleId`, voice `speaker`). Always discover them via the listing endpoints above. - Do not use `Bearer` in the Authorization header — Pictory uses the raw key. - Do not omit `videoName` (required) on render endpoints. - Do not include both `brandId` and `brandName` in the same request — the API rejects this. - Do not place `avatarId` inside a scene's `avatar` field. The avatar identity is set once at the top level (`avatar.avatarId`); per-scene `avatar` accepts only position and styling overrides. - API render jobs are NOT saved as projects unless `saveProject: true` is passed (or `templateId` is provided to render against an existing project). ## Reference Documentation For complete details, refer to `https://docs.pictory.ai/llms-full.txt` and `https://docs.pictory.ai/openapi.json`. ``` *** ## 4. Pictory MCP Server For agentic LLM tools (Claude Desktop, Cursor, Windsurf, custom agents built on the MCP protocol), Pictory provides an **MCP server** that exposes the API as structured tools. The agent can invoke endpoints directly without you having to handcraft requests. Learn more and grab the connection details at [Pictory MCP Server](https://pictory.ai/pictory-mcp-server-api). Setup pages for popular agentic tools: * [Claude Code setup](/ai-tools/claude-code) * [Cursor setup](/ai-tools/cursor) * [Windsurf setup](/ai-tools/windsurf) *** ## 5. Example Prompts and Expected API Calls These examples show the natural-language input a user might give and the API call an LLM (configured per the system prompt above) should produce. ### Example 1 — Simple text-to-video > "Create a 30-second product demo video about our new coffee maker. Use an upbeat AI voice." Expected endpoint: `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Coffee Maker Demo", "language": "en", "aspectRatio": "16:9", "scenes": [ { "story": "Introducing the BrewMax 3000 — your morning, simplified.", "createSceneOnEndOfSentence": true }, { "story": "One-touch brewing, three temperature presets, and a sleek glass carafe.", "createSceneOnEndOfSentence": true }, { "story": "BrewMax 3000. Order yours today.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` ### Example 2 — PPT-to-video in another language > "Convert this PowerPoint deck into a French training video with speaker notes as narration: [https://example.com/training.pptx](https://example.com/training.pptx)" Expected endpoint: `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "French Training Video", "language": "fr", "scenes": [ { "pptUrl": "https://example.com/training.pptx", "useSpeakerNotes": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Gabriel", "speed": 100, "amplificationLevel": 0 }] } } ``` ### Example 3 — Avatar video using a saved template > "Use my brand template and render a video where my Chef avatar walks through 4 recipe steps." Before generating the payload, the LLM should fetch the available avatars and templates so it uses real IDs from the user's account: ```bash theme={null} curl -H 'Authorization: YOUR_API_KEY' https://api.pictory.ai/pictoryapis/v1/avatars curl -H 'Authorization: YOUR_API_KEY' https://api.pictory.ai/pictoryapis/v2/projects ``` Then build the payload with real IDs (the placeholders below are stand-ins for values picked from the discovery responses): Expected endpoint: `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Recipe — Lemon Risotto", "templateId": "", "avatar": { "avatarId": "" }, "scenes": [ { "story": "Today we are making lemon risotto. Here are the ingredients." }, { "story": "Step 1: Heat the pan and toast the rice." }, { "story": "Step 2: Add lemon zest and stock, stirring continuously." }, { "story": "Step 3: Plate and serve with parmesan." } ] } ``` ### Example 4 — Blog URL to video > "Turn this blog post into a one-minute video with subtitles: [https://example.com/blog/our-launch](https://example.com/blog/our-launch)" Expected endpoint: `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Blog to Video — Launch Post", "language": "en", "aspectRatio": "16:9", "scenes": [ { "blogUrl": "https://example.com/blog/our-launch" } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` The Pictory backend fetches and summarizes the blog content automatically. The resulting scenes are derived from the article structure. Each scene in the `scenes` array must contain exactly one content source — `story`, `blogUrl`, `pptUrl`, `audioUrl`, `videoUrl`, or `storyCoPilot` — never mix sources within a single scene. ### Example 5 — Storyboard preview, then render-from-preview > "Generate a preview first so I can review the scenes before paying for a full render." This is a two-step flow. The LLM should produce both steps. **Step 1 — Create the preview:** Expected endpoint: `POST /v2/video/storyboard` ```json theme={null} { "videoName": "Preview-First Demo", "language": "en", "scenes": [ { "story": "First scene narration text.", "createSceneOnEndOfSentence": true }, { "story": "Second scene narration text.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` The response contains a `jobId`. Poll `GET /v1/jobs/{jobid}` until the preview is `completed`; the response includes the storyboard scenes and metadata. **Step 2 — Render from the preview:** Expected endpoint: `PUT /v2/video/render/{storyboardjobid}` ```bash theme={null} curl --request PUT \ --url 'https://api.pictory.ai/pictoryapis/v2/video/render/' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "webhook": "https://your-domain.com/pictory-webhook" }' ``` The path parameter is the **preview job ID** from Step 1, not a project ID. The request body is optional — pass `webhook` here only if you want to override the webhook URL set during the preview step. Use this two-step flow when the user wants to review or edit scenes before committing render resources. To edit scenes between the two steps, see the [Update Storyboard Elements API](/api-reference/video-storyboard/update-storyboard-elements). ### Example 6 — Polling job status > "How do I check if my video at job ID `9b1c4d2e-7f8a-4321-b2c3-d456e789f012` is done?" Expected endpoint: `GET /v1/jobs/{jobid}` ```bash theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/9b1c4d2e-7f8a-4321-b2c3-d456e789f012' \ --header 'Authorization: YOUR_API_KEY' ``` Poll every 10–30 seconds. When `data.status === "completed"`, the rendered video URL is in `data.videoURL`. For long-running renders, prefer passing a `webhook` URL in the render request body instead of polling. *** ## 6. Troubleshooting LLM Output | Symptom | Cause | Fix | | -------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | LLM uses `Bearer YOUR_API_KEY` | Default training bias toward OAuth/Bearer flows | Restate the auth rule in your system prompt: "The `Authorization` header value is the raw key, no prefix." | | LLM invents an endpoint | Insufficient context | Feed `llms-full.txt` or `openapi.json` into the session | | Render succeeds but video does not appear in My Projects | Missing `saveProject: true` and no `templateId` | Tell the LLM: "Always include `saveProject: true` unless the user passes a `projectId` as `templateId`." | | LLM passes both `brandId` and `brandName` | Field-level constraint not in training data | Reinforce in system prompt; the API rejects both-together | | Polling returns 404 | Wrong job type or wrong account | Verify the API key matches the account that submitted the job | *** ## Next Steps Connect agentic tools directly to the Pictory API Wire Claude Code into the Pictory API Configure Cursor to call Pictory endpoints Configure Windsurf for Pictory automation Complete working JSON payloads for common use cases Endpoint-by-endpoint reference # PowerPoint to Video with AI Voice-Over Source: https://docs.pictory.ai/guides/presentation-to-video/powerpoint-ai-voiceover Convert PowerPoint presentations into videos with AI-generated voice-over narration This guide shows you how to convert PowerPoint presentations into engaging videos with professional AI-generated voice-over narration. Perfect for creating training videos, online courses, or shareable presentations from your existing slide decks. ## What You'll Learn Convert PowerPoint slides into video scenes Add professional narration automatically Each slide becomes a scene with narration Automatic text extraction and voice generation ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * PowerPoint file accessible via public URL * Basic understanding of voice-over concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How PowerPoint-to-Video Works When you convert a PowerPoint presentation to video: 1. **File Processing** - Your PPT file is accessed and parsed 2. **Slide Extraction** - Each slide becomes a separate video scene 3. **Text Extraction** - Text content is extracted from slides for narration 4. **Visual Preservation** - Slide designs and visuals are maintained 5. **Voice Generation** - AI creates natural narration from extracted text 6. **Video Rendering** - Final video is assembled with voice-over synchronized to slides The PowerPoint file must be accessible via a public URL. Upload your file to cloud storage (Google Drive, Dropbox, AWS S3) and use the public share link. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample PPT URL - replace with your own PPT file URL const PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt.pptx"; async function createPptToVideoWithVoiceOver() { try { console.log("Creating video from PowerPoint with AI voice-over..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ppt_to_video_with_voiceover", scenes: [ { pptUrl: PPT_URL, // PowerPoint file URL at scene level } ], // Voice-over configuration voiceOver: { enabled: true, // Enable voice-over aiVoices: [ { speaker: "Brian", // AI voice name speed: 100, // Normal speaking speed amplificationLevel: 0, // Normal volume }, ], }, }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video from PowerPoint is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createPptToVideoWithVoiceOver(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample PPT URL - replace with your own PPT file URL PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt.pptx" def create_ppt_to_video_with_voiceover(): try: print("Creating video from PowerPoint with AI voice-over...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ppt_to_video_with_voiceover', 'scenes': [ { 'pptUrl': PPT_URL # PowerPoint file URL at scene level } ], # Voice-over configuration 'voiceOver': { 'enabled': True, # Enable voice-over 'aiVoices': [ { 'speaker': 'Brian', # AI voice name 'speed': 100, # Normal speaking speed 'amplificationLevel': 0 # Normal volume } ] } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video from PowerPoint is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_ppt_to_video_with_voiceover() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `language` | string | No | Language for AI-generated narration. Default: `en`. Options: `zh`, `nl`, `en`, `fr`, `de`, `hi`, `it`, `ja`, `ko`, `mr`, `pt`, `ru`, `es`, `ta` | | `scenes` | array | Yes | Array of scene objects | | `voiceOver` | object | No | Voice-over configuration (omit for video without narration) | ### Scene Parameters | Parameter | Type | Required | Description | | ------------ | ------- | -------- | --------------------------------------------------------------- | | `pptUrl` | string | Yes | Public URL to the PowerPoint file (.ppt or .pptx) | | `animatePPT` | boolean | No | Enable AI-generated slide animations. Only valid with `pptUrl`. | ### Voice-Over Configuration | Parameter | Type | Required | Description | | -------------------- | ------- | -------- | ------------------------------------------ | | `enabled` | boolean | Yes | Set to `true` to enable voice-over | | `aiVoices` | array | Yes | Array of AI voice configurations | | `speaker` | string | Yes | AI voice name (e.g., "Brian", "Emma") | | `speed` | number | No | Voice speed 50-200 (default: 100 = normal) | | `amplificationLevel` | number | No | Volume level -1 to 1 (default: 0 = normal) | ## Supported File Formats | Format | Extension | Description | | ------------------ | --------- | -------------------------------------- | | PowerPoint 2007+ | `.pptx` | Modern PowerPoint format (recommended) | | PowerPoint 97-2003 | `.ppt` | Legacy PowerPoint format | For best compatibility and processing speed, use `.pptx` format. Convert older `.ppt` files to `.pptx` before uploading. ## Common Use Cases ### Training and Education ```javascript theme={null} { videoName: "employee_training_video", scenes: [{ pptUrl: "https://storage.example.com/employee-training.pptx" }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", // Clear female voice speed: 95, // Slightly slower for learning amplificationLevel: 0 }] } } ``` **Result:** Educational video with clear, measured narration perfect for training. ### Sales Presentations ```javascript theme={null} { videoName: "product_pitch_video", scenes: [{ pptUrl: "https://storage.example.com/product-pitch.pptx" }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", // Professional male voice speed: 105, // Slightly faster for energy amplificationLevel: 0.2 // Louder for emphasis }] } } ``` **Result:** Dynamic sales video with energetic, engaging narration. ### Webinar Content ```javascript theme={null} { videoName: "webinar_slides_video", scenes: [{ pptUrl: "https://storage.example.com/webinar-slides.pptx" }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Joanna", // Friendly female voice speed: 100, // Normal pace amplificationLevel: 0 }] } } ``` **Result:** Professional webinar video with natural narration. ## Best Practices Optimize your slides for video conversion: * **Clear Text**: Use readable fonts and sufficient font size (24pt minimum) * **Simple Layouts**: Avoid overly complex slide designs * **Concise Content**: Keep text brief - narration works better with short phrases * **Consistent Style**: Use consistent formatting across all slides * **High Quality Images**: Use high-resolution images that look good in video Ensure your PowerPoint file can be accessed: * **Cloud Storage**: Upload to Google Drive, Dropbox, OneDrive, or AWS S3 * **Public Link**: Generate a public sharing link (no login required) * **Direct URL**: Use the direct file URL, not a preview or viewer link * **Test Access**: Open the URL in an incognito browser to verify public access * **Stable URL**: Ensure the link will not expire during processing Select voice settings that match your content: * **Training/Education**: Use slower speeds (90-100) for comprehension * **Sales/Marketing**: Use slightly faster speeds (105-115) for energy * **Technical Content**: Use clear voices like Brian or Emma * **Consistency**: Use the same voice across related presentations * **Test First**: Create a sample video to preview voice quality Structure your slides for voice-over: * **Full Sentences**: Write complete sentences, not just bullet points * **Natural Language**: Text should sound natural when read aloud * **Proper Punctuation**: Use periods, commas for natural pauses * **Avoid Abbreviations**: Spell out acronyms on first use * **Logical Flow**: Ensure text flows naturally from slide to slide Plan for appropriate video duration: * **Short Presentations**: 5-10 slides work well for social media * **Medium Presentations**: 15-25 slides for training or tutorials * **Long Presentations**: Break 30+ slide decks into multiple videos * **Processing Time**: More slides = longer processing time * **Viewer Attention**: Keep videos under 10 minutes for better engagement ## Troubleshooting **Problem:** The API cannot download or access your PPT file. **Solution:** * Verify the URL is publicly accessible (test in incognito browser) * Check that the URL is a direct file link, not a preview page * For Google Drive: Right-click → Share → "Anyone with the link" * For Dropbox: Use the direct download link, not the preview link * Ensure the file hasn't expired or been deleted * Try re-uploading the file and generating a new link **Problem:** Video does not include all slides or they are out of sequence. **Solution:** * Verify all slides are present in the original PowerPoint * Check for hidden slides in PowerPoint - unhide them before uploading * Ensure slide numbers are sequential * Re-save the PowerPoint file and upload again * Try exporting as .pptx if using an older .ppt format **Problem:** Narration seems unrelated to what is shown on slides. **Solution:** * The AI narrates visible text on slides * Check that slides contain actual text content (not just images) * If using images with embedded text, add actual text boxes * For slides with minimal text, consider using speaker notes instead * See the [PowerPoint with Speaker Notes](/guides/presentation-to-video/powerpoint-speaker-notes) guide **Problem:** Video plays slides with no voice-over. **Solution:** * Verify those slides contain text in PowerPoint * Text must be in actual text boxes, not part of images * Add descriptive text to image-only slides * Or use the `useSpeakerNotes: true` parameter with speaker notes * Check that text is not white-on-white or otherwise hidden **Problem:** Job status shows "in-progress" for extended periods. **Solution:** * PowerPoint processing time depends on number of slides * Expected times: * 5-10 slides: 5-8 minutes * 15-20 slides: 10-15 minutes * 30+ slides: 20-30 minutes * Large file sizes (with many images) take longer * Check job status every 5-10 seconds (not more frequently) * If stuck for over an hour, contact support with job ID ## Next Steps Enhance your PowerPoint videos with these features: Use speaker notes instead of slide text for narration Add AI-generated animations and multilingual narration Add music to complement your presentation Apply consistent branding to all your videos ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) - List available AI voices * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # PowerPoint to Video with Animated Slides Source: https://docs.pictory.ai/guides/presentation-to-video/powerpoint-animated-slides Convert PowerPoint presentations into videos with AI-generated slide animations and motion graphics This guide shows you how to convert PowerPoint presentations into dynamic videos with AI-generated animations. Instead of static slide images, the AI analyzes each slide and creates motion graphics — zoom, pan, and fade effects — that bring your slides to life. ## What You Will Learn AI generates motion graphics for each slide Generate narration in 14 languages Mix animations with speaker notes and language options Automatic animation generation per slide ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * PowerPoint file accessible via public URL ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Animated Slides Work When you enable `animatePPT`: 1. **File Processing** - Your PPT file is accessed and parsed 2. **Slide Extraction** - Each slide becomes a video scene 3. **AI Analysis** - The AI analyzes each slide's visual content 4. **Animation Generation** - Motion graphics prompts are generated per slide (zoom, pan, fade effects) 5. **Narration** - Voice-over text is generated (from slide text or speaker notes) 6. **Video Rendering** - Final video combines animated slides with narration The `animatePPT` option uses AI to generate animation prompts for each slide. This works for slides with image backgrounds. Video-based slides are not animated. ## Complete Example — Animated PPT ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt.pptx"; async function createAnimatedPptVideo() { try { console.log("Creating animated video from PowerPoint..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "animated_ppt_video", scenes: [ { pptUrl: PPT_URL, animatePPT: true, // Enable AI slide animations } ], voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 100, amplificationLevel: 0, }, ], }, }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Job ID:", jobId); // Poll for completion let jobCompleted = false; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; console.log("Video URL:", statusResponse.data.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed"); } await new Promise(resolve => setTimeout(resolve, 10000)); } } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createAnimatedPptVideo(); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt.pptx" def create_animated_ppt_video(): try: print("Creating animated video from PowerPoint...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'animated_ppt_video', 'scenes': [ { 'pptUrl': PPT_URL, 'animatePPT': True # Enable AI slide animations } ], 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 100, 'amplificationLevel': 0 } ] } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print(f"Job ID: {job_id}") # Poll for completion job_completed = False while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True print(f"Video URL: {status_response.json()['data']['videoURL']}") elif status == 'failed': raise Exception("Video creation failed") time.sleep(10) except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_animated_ppt_video() ``` ## Multilingual PPT Video Use the `language` parameter to generate narration in a different language. The AI reads each slide and generates narration directly in the target language. ```javascript Node.js theme={null} { videoName: "german_ppt_video", language: "de", // Generate narration in German scenes: [ { pptUrl: "https://storage.example.com/presentation.pptx", animatePPT: true // Optional: add animations } ], voiceOver: { enabled: true, aiVoices: [{ speaker: "Vicki", // German voice speed: 100, amplificationLevel: 0 }] } } ``` ```python Python theme={null} { 'videoName': 'german_ppt_video', 'language': 'de', # Generate narration in German 'scenes': [ { 'pptUrl': 'https://storage.example.com/presentation.pptx', 'animatePPT': True # Optional: add animations } ], 'voiceOver': { 'enabled': True, 'aiVoices': [{ 'speaker': 'Vicki', # German voice 'speed': 100, 'amplificationLevel': 0 }] } } ``` ### Supported Languages | Code | Language | | ---- | ----------------- | | `en` | English (default) | | `zh` | Chinese | | `nl` | Dutch | | `fr` | French | | `de` | German | | `hi` | Hindi | | `it` | Italian | | `ja` | Japanese | | `ko` | Korean | | `mr` | Marathi | | `pt` | Portuguese | | `ru` | Russian | | `es` | Spanish | | `ta` | Tamil | ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | -------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `language` | string | No | Language for AI-generated narration. Default: `en` | | `scenes` | array | Yes | Array of scene objects | | `voiceOver` | object | No | Voice-over configuration | ### Scene Parameters | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------- | | `pptUrl` | string | Yes | Public URL to the PowerPoint file | | `animatePPT` | boolean | No | Enable AI-generated slide animations. Only valid with `pptUrl`. Default: `false` | | `useSpeakerNotes` | boolean | No | Use speaker notes for narration instead of slide text. Only valid with `pptUrl`. Default: `false` | ## Common Use Cases ### Animated with Speaker Notes ```javascript theme={null} { videoName: "animated_with_notes", scenes: [{ pptUrl: "https://storage.example.com/visual-deck.pptx", animatePPT: true, useSpeakerNotes: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 100, amplificationLevel: 0 }] } } ``` **Result:** Animated slides with narration from speaker notes. The AI generates animation prompts while using your speaker notes for the voice-over. ### Multilingual Animated Presentation ```javascript theme={null} { videoName: "french_animated_ppt", language: "fr", scenes: [{ pptUrl: "https://storage.example.com/product-deck.pptx", animatePPT: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Celine", speed: 100, amplificationLevel: 0 }] } } ``` **Result:** The AI reads your slides and generates French narration with animated slide transitions. ### Japanese Training Video ```javascript theme={null} { videoName: "japanese_training", language: "ja", scenes: [{ pptUrl: "https://storage.example.com/training.pptx", useSpeakerNotes: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Mizuki", speed: 95, amplificationLevel: 0 }] } } ``` **Result:** Training video with Japanese narration generated from speaker notes. ## Feature Combinations | `animatePPT` | `useSpeakerNotes` | `language` | Behavior | | ------------ | ----------------- | ---------- | -------------------------------------------------------------- | | `false` | `false` | `en` | Standard PPT to video — slide text used as-is for narration | | `false` | `true` | `en` | Speaker notes used for narration, no animations | | `true` | `false` | `en` | AI generates animations + uses slide text for narration | | `true` | `true` | `en` | AI generates animations only, speaker notes used for narration | | `false` | `false` | `de` | AI generates German narration from slide content | | `true` | `false` | `de` | AI generates German narration + slide animations | | `true` | `true` | `de` | AI generates animations, speaker notes used for narration | When both `animatePPT` and `useSpeakerNotes` are `true`, the AI focuses on generating animation prompts and uses your speaker notes as the narration source rather than generating new text. ## Best Practices Get the most out of AI animations: * **Clear Visual Layout**: Simple, well-structured slides produce better animations * **High Quality Images**: Use high-resolution images for smooth motion effects * **Minimal Text Overlay**: Slides with large visuals animate more effectively * **Consistent Design**: Similar slide designs produce cohesive animation styles Match your voice to the language: * Use language-native AI voices for the best pronunciation * Browse available voices with the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) API * Test with a short presentation before converting a full deck * Adjust voice speed for the target language (some languages are naturally faster/slower) `animatePPT` works best for: * **Image-heavy slides**: Photos, charts, and diagrams come alive with motion * **Marketing presentations**: Add visual energy to product decks * **Social media content**: Animated slides are more engaging for short-form video * **Training materials**: Motion helps maintain viewer attention Skip animations for: * **Text-heavy slides**: Motion can distract from reading * **Data tables**: Complex tables are better shown static * **Short presentations**: 2–3 slides may not benefit much from animation ## Next Steps Basic PowerPoint to video conversion Use speaker notes for narration Add music to your presentation video Apply consistent branding ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview) - Preview before rendering * [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) - List available AI voices # PowerPoint to Video with Speaker Notes Source: https://docs.pictory.ai/guides/presentation-to-video/powerpoint-speaker-notes Convert PowerPoint presentations into videos using speaker notes for voice-over narration This guide shows you how to create videos from PowerPoint files using speaker notes for voice-over narration instead of slide text. Perfect when your slides contain minimal text or visual-heavy designs, but you have detailed explanations in the speaker notes. ## What You'll Learn Convert PowerPoint files to video automatically Use speaker notes for voice-over narration Generate professional narration from notes Choose between slide text or speaker notes ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * PowerPoint file with speaker notes accessible via public URL * Basic understanding of PowerPoint speaker notes ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Speaker Notes Narration Works When you use speaker notes for narration: 1. **File Processing** - Your PPT file is accessed and parsed 2. **Slide Extraction** - Each slide becomes a video scene 3. **Notes Extraction** - Speaker notes are extracted from each slide 4. **Visual Preservation** - Slide designs remain intact as scene backgrounds 5. **Voice Generation** - AI creates narration from speaker notes, not slide text 6. **Video Rendering** - Final video combines slides with notes-based narration Speaker notes provide more natural, conversational narration compared to reading slide bullet points. This is ideal for presentations designed for visual impact where notes contain the full explanation. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample PPT URL - replace with your own PPT file URL const PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt_with_notes.pptx"; async function createPptToVideoWithSpeakerNotes() { try { console.log("Creating video from PowerPoint with speaker notes..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ppt_to_video_speaker_notes", scenes: [ { pptUrl: PPT_URL, // PowerPoint file URL at scene level useSpeakerNotes: true, // Use speaker notes for narration } ], // Voice-over configuration voiceOver: { enabled: true, // Enable voice-over aiVoices: [ { speaker: "Brian", // AI voice name speed: 100, // Normal speaking speed amplificationLevel: 0, // Normal volume }, ], }, }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video from PowerPoint with speaker notes is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createPptToVideoWithSpeakerNotes(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample PPT URL - replace with your own PPT file URL PPT_URL = "https://pictory-static.pictorycontent.com/sample_ppt_with_notes.pptx" def create_ppt_to_video_with_speaker_notes(): try: print("Creating video from PowerPoint with speaker notes...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ppt_to_video_speaker_notes', 'scenes': [ { 'pptUrl': PPT_URL, # PowerPoint file URL at scene level 'useSpeakerNotes': True # Use speaker notes for narration } ], # Voice-over configuration 'voiceOver': { 'enabled': True, # Enable voice-over 'aiVoices': [ { 'speaker': 'Brian', # AI voice name 'speed': 100, # Normal speaking speed 'amplificationLevel': 0 # Normal volume } ] } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video from PowerPoint with speaker notes is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_ppt_to_video_with_speaker_notes() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `language` | string | No | Language for AI-generated narration. Default: `en`. Options: `zh`, `nl`, `en`, `fr`, `de`, `hi`, `it`, `ja`, `ko`, `mr`, `pt`, `ru`, `es`, `ta` | | `scenes` | array | Yes | Array of scene objects | | `voiceOver` | object | No | Voice-over configuration | ### Scene Parameters | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `pptUrl` | string | Yes | Public URL to the PowerPoint file | | `useSpeakerNotes` | boolean | No | Set to `true` to use speaker notes for narration (default: `false` = uses slide text). Only valid when `pptUrl` is provided. | | `animatePPT` | boolean | No | Enable AI-generated slide animations. Only valid with `pptUrl`. | ### Voice-Over Configuration | Parameter | Type | Required | Description | | -------------------- | ------- | -------- | ---------------------------------- | | `enabled` | boolean | Yes | Set to `true` to enable voice-over | | `aiVoices` | array | Yes | Array of AI voice configurations | | `speaker` | string | Yes | AI voice name | | `speed` | number | No | Voice speed 50-200 (default: 100) | | `amplificationLevel` | number | No | Volume level -1 to 1 (default: 0) | ## Speaker Notes vs Slide Text | Configuration | Narration Source | Best Used For | | ---------------------------------- | ------------------ | ----------------------------------------------------------------- | | `useSpeakerNotes: false` (default) | Visible slide text | Slides with complete sentences, text-heavy presentations | | `useSpeakerNotes: true` | Speaker notes | Visual-heavy slides, bullet points only, conversational narration | **Important:** If a slide has no speaker notes and `useSpeakerNotes` is set to `true`, that slide will have no voice-over narration. Ensure all slides have speaker notes before using this feature. ## Common Use Cases ### Training Videos with Detailed Explanations ```javascript theme={null} { videoName: "training_with_notes", scenes: [{ pptUrl: "https://storage.example.com/training-deck.pptx", useSpeakerNotes: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 95, // Slower for training amplificationLevel: 0 }] } } ``` **Result:** Slides show bullet points while detailed explanations play as narration. ### Visual-Heavy Presentations ```javascript theme={null} { videoName: "infographic_presentation", scenes: [{ pptUrl: "https://storage.example.com/infographic-deck.pptx", useSpeakerNotes: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 100, amplificationLevel: 0 }] } } ``` **Result:** Clean visual slides with comprehensive narration from notes. ### Webinar Content ```javascript theme={null} { videoName: "webinar_with_notes", scenes: [{ pptUrl: "https://storage.example.com/webinar-slides.pptx", useSpeakerNotes: true }], voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", speed: 105, // Slightly faster for engagement amplificationLevel: 0.1 }] } } ``` **Result:** Professional webinar with presenter-style narration. ## Best Practices Choose speaker notes narration when: * **Visual Slides**: Slides contain mainly images, charts, or diagrams * **Bullet Points**: Slides have brief bullet points needing elaboration * **Conversational Tone**: You want natural, flowing narration * **Detailed Explanations**: Notes contain the full explanation * **Professional Format**: Slides designed for visual impact, not reading Use slide text narration (`useSpeakerNotes: false`) when: * **Complete Sentences**: Slides contain full text meant to be read * **No Notes**: Speaker notes are empty or minimal * **Simple Content**: Text on slides is self-explanatory * **Quote-Based**: Slides contain quotes or specific text to emphasize Optimize your speaker notes for AI narration: * **Conversational Style**: Write as you would speak, not formal bullets * **Complete Sentences**: Use full sentences with proper punctuation * **Natural Flow**: Ensure smooth transitions between ideas * **Appropriate Length**: 2-4 sentences per slide (30-60 seconds narration) * **Clear Pronunciation**: Spell out acronyms and difficult terms **Good Example:** "This chart shows our quarterly revenue growth. Notice how Q3 exceeded expectations by 25%, driven primarily by our new product launch. This trend is expected to continue through the end of the year." **Poor Example:** "Q3 rev up 25% - new prod launch - trend continues" Set up your presentation for speaker notes conversion: * **Add Notes to All Slides**: Every slide should have speaker notes * **Consistent Length**: Keep notes roughly similar in length for pacing * **Test Notes**: Read notes aloud to ensure they sound natural * **Avoid Redundancy**: Do not repeat exactly what is on the slide * **Link Slides**: Notes should transition smoothly between slides Create effective slide-notes combinations: * **Slides**: Show key points, visuals, data * **Notes**: Provide context, explanations, details * **Complement**: Notes should expand on visuals, not duplicate them * **Focus**: Keep slides visual, notes verbal * **Engagement**: Use notes to tell the story behind the visuals ## Troubleshooting **Problem:** Certain slides play with no voice-over. **Solution:** * Check that those slides have speaker notes in PowerPoint * Speaker notes must contain actual text (not be empty) * View → Notes Page in PowerPoint to verify notes exist * Add speaker notes to all slides before conversion * If you want some slides silent, add a note like "\[pause]" or "\[music only]" **Problem:** The voice-over seems different from your speaker notes. **Solution:** * Verify you set `useSpeakerNotes: true` in your request * Check that PowerPoint notes are in the Notes section, not text boxes on slides * Ensure notes were not accidentally placed in slide master or layout * Re-save PowerPoint file and try again * Use PowerPoint's Notes Page view to verify notes are properly saved **Problem:** Long speaker notes seem truncated in narration. **Solution:** * Very long notes (500+ words per slide) may be shortened * Break long presentations into multiple videos * Keep speaker notes to 30-60 seconds of narration per slide * Distribute content across more slides if needed * Aim for 50-150 words per slide's notes **Problem:** AI narration does not flow smoothly. **Solution:** * Write speaker notes in a conversational, natural tone * Use complete sentences with proper punctuation * Avoid abbreviations, acronyms, or technical shorthand * Read notes aloud before conversion to check flow * Adjust voice speed (95-105) for more natural pacing * Try different AI voices to find the best match **Problem:** Not sure how to add or view speaker notes. **Solution:** * **Add Notes**: In PowerPoint, click in the Notes section below each slide * **View Notes**: Go to View → Notes Page for full editor * **Normal View**: Notes appear in bottom panel in Normal view * **Slide Sorter**: Switch to Normal view to see/edit notes * **Mac Users**: View → Normal, notes panel is below slide * **Save**: Always save PowerPoint file after adding notes ## Next Steps Enhance your speaker notes videos with these features: Use slide text instead of speaker notes Add AI-generated animations and multilingual narration Add music to complement narration Apply consistent branding to videos ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) - List available AI voices * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # End-to-End Recipes Source: https://docs.pictory.ai/guides/recipes/end-to-end-recipes Complete, runnable JSON payloads for the most common Pictory API use cases This page collects **complete, working request payloads** for the most common Pictory API scenarios. Each recipe has: * A one-line description of the use case * The full request body, ready to send * The endpoint and a one-line cURL * A note on how to retrieve the rendered video Replace `YOUR_API_KEY` with your `pictai_` API key from the [API Access page](https://app.pictory.ai/api-access). Every recipe uses the same authentication pattern: `Authorization: YOUR_API_KEY` (no `Bearer` prefix). After submitting a render, the response includes a `jobId`. Poll `GET /v1/jobs/{jobid}` every 10–30 seconds until `status` is `completed`, OR include a `webhook` URL in the request body to receive the result automatically. *** ## Recipe 1 — Text-to-Video with AI Voice-Over **Use case:** Turn a short script into a narrated video with auto-selected stock visuals. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Product Launch Teaser", "language": "en", "aspectRatio": "16:9", "saveProject": true, "scenes": [ { "story": "Introducing our latest innovation. Designed for creators, built for scale.", "createSceneOnEndOfSentence": true }, { "story": "Launch your next project in minutes, not weeks.", "createSceneOnEndOfSentence": true }, { "story": "Available today. Visit our website to learn more.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] }, "backgroundMusic": { "enabled": true, "autoMusic": true, "volume": 0.1 } } ``` ```bash theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v2/video/storyboard/render' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data @payload.json ``` *** ## Recipe 2 — Multilingual Video (Generate Narration in a Target Language) **Use case:** Render the same video in French (or any of 14 supported languages) with native-language narration and voice. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Lancement Produit", "language": "fr", "aspectRatio": "16:9", "scenes": [ { "story": "Découvrez notre dernière innovation. Conçue pour les créateurs, construite pour évoluer.", "createSceneOnEndOfSentence": true }, { "story": "Lancez votre prochain projet en quelques minutes, pas en semaines.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Gabriel", "speed": 100, "amplificationLevel": 0 }] } } ``` **Supported languages:** `zh, nl, en, fr, de, hi, it, ja, ko, mr, pt, ru, es, ta` ### Default Male STD Voice by Language If you do not specify a voice, use the documented default male STD voice for the target language. For languages without a documented default, the API falls back to the English voice — in that case, prefer calling `GET /v1/voiceovers/tracks` to pick a native voice. | `language` | Default voice (`speaker`) | | ---------------------------- | ---------------------------------------------------------- | | `en` | `Martin` | | `nl` | `Tim` | | `fr` | `Gabriel` | | `de` | `Wilbur` | | `it` | `Marco` | | `pt` | `Aurelio` | | `es` | `Hugo` | | `hi`, `ru` | `Martin` (English fallback — no native STD default) | | `zh`, `ja`, `ko`, `mr`, `ta` | None documented — discover via `GET /v1/voiceovers/tracks` | *** ## Recipe 3 — PowerPoint to Video with Speaker Notes **Use case:** Convert a PowerPoint deck into a video using the slides as visuals and the speaker notes as narration. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Quarterly Training", "language": "en", "scenes": [ { "pptUrl": "https://example.com/training-deck.pptx", "useSpeakerNotes": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` Set `animatePPT: true` inside the scene to enable AI-generated slide animations (zoom, pan, fade) for image-heavy slides. See the [Animated Slides guide](/guides/presentation-to-video/powerpoint-animated-slides) for details. *** ## Recipe 4 — Avatar Video (Chef Walking Through a Recipe) **Use case:** Render a how-to video where an AI avatar narrates the steps. Each step becomes its own scene; the avatar reads the scene's `story` text. The avatar is configured once at the top level and applies across all scenes. Avatar IDs are account-scoped. Before rendering, fetch the list of avatars available to your account with `GET /v1/avatars` and use a real `avatarId` from that response. The `YOUR_AVATAR_ID` placeholder below is not a valid identifier — replace it with an ID from your account. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Recipe — Lemon Risotto", "language": "en", "aspectRatio": "16:9", "saveProject": true, "avatar": { "avatarId": "YOUR_AVATAR_ID", "position": "bottom-right" }, "scenes": [ { "story": "Today we are making lemon risotto. You will need arborio rice, fresh lemon, parmesan, and warm vegetable stock." }, { "story": "Step 1: Heat olive oil in a heavy pan and toast the rice for two minutes." }, { "story": "Step 2: Add warm stock one ladle at a time, stirring continuously until absorbed." }, { "story": "Step 3: Off the heat, stir in lemon zest, juice, and grated parmesan. Plate and serve." } ] } ``` Fetch available avatar IDs via [`GET /v1/avatars`](/api-reference/avatars/get-avatars). To hide the avatar in a specific scene or reposition it per scene, use the per-scene `avatar` override field (e.g., `"avatar": { "hide": true }` or `"avatar": { "position": "top-left" }`). The per-scene override only accepts position and styling fields — set the avatar identity once at the top level. *** ## Recipe 5 — Render Against an Existing Project as a Template **Use case:** You built a master template in the Pictory web app and want every new video to inherit its branding, layouts, music, and transitions. Pass the template's `projectId` as `templateId`. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Recipe — Tomato Soup", "templateId": "YOUR_TEMPLATE_PROJECT_ID", "avatar": { "avatarId": "YOUR_AVATAR_ID" }, "scenes": [ { "story": "Tomato soup, made simple. Here is what you need." }, { "story": "Step 1: Roast the tomatoes with garlic and olive oil." }, { "story": "Step 2: Blend with warm stock until smooth." }, { "story": "Step 3: Season, garnish with basil, and serve." } ] } ``` Use `templateId` when you want to **reuse an existing project as a template with new content.** Use `saveProject: true` when you want the render to create a **new** project in My Projects. These two options serve different purposes and can be combined. *** ## Recipe 6 — Re-render a Saved Project As-Is **Use case:** You have an existing project saved in your Pictory dashboard and you just want to re-render it without changes (e.g., to regenerate the video file after editing branding). **Endpoint:** `POST /v2/projects/{projectid}/render` ```bash theme={null} curl --request POST \ --url 'https://api.pictory.ai/pictoryapis/v2/projects/YOUR_PROJECT_ID/render' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' ``` No request body is needed — the project's saved configuration is used as-is. *** ## Recipe 7 — Brand Kit Applied via brandName **Use case:** Apply your saved brand kit (colors, fonts, logo, intro/outro) to a video. You can identify the brand by either `brandId` or `brandName` — not both. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Branded Promo", "language": "en", "brandName": "Acme Corp Brand", "scenes": [ { "story": "Acme. Built for tomorrow.", "createSceneOnEndOfSentence": true }, { "story": "Discover what's new this quarter.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` To list available brand kits and their IDs, call [`GET /v1/brands/video`](/api-reference/branding/get-video-brands). Do not provide both `brandId` and `brandName` in the same request. The API will reject the request with a validation error. *** ## Recipe 8 — Webhook-Driven Async Workflow **Use case:** You do not want to poll for job status. Pass a `webhook` URL and Pictory will POST the result to your server when the render completes. **Endpoint:** `POST /v2/video/storyboard/render` ```json theme={null} { "videoName": "Async Render", "scenes": [ { "story": "Async render demonstration video.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] }, "webhook": "https://your-domain.com/pictory-webhook", "webhookInput": { "internalId": "abc-123", "source": "campaign-builder" } } ``` **Webhook requirements:** * Publicly reachable HTTPS URL * Accepts `POST` with `Content-Type: application/json` * Responds with a `2xx` status within 30 seconds * The `webhookInput` object you pass is echoed back in the callback payload — use it for correlation *** ## Recipe 9 — Create a Storyboard Preview Before Rendering **Use case:** Generate scene thumbnails and a project structure for review before committing to a full render. **Endpoint:** `POST /v2/video/storyboard` ```json theme={null} { "videoName": "Preview First", "scenes": [ { "story": "First scene narration text.", "createSceneOnEndOfSentence": true }, { "story": "Second scene narration text.", "createSceneOnEndOfSentence": true } ], "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Martin", "speed": 100, "amplificationLevel": 0 }] } } ``` After the preview job completes, you can: 1. Review the scenes via [`GET /v1/jobs/{jobid}`](/api-reference/jobs/get-storyboard-preview-job-by-id) 2. Edit scenes via [`PUT /v2/video/storyboard/elements`](/api-reference/video-storyboard/update-storyboard-elements) 3. Render with [`PUT /v2/video/render/{storyboardjobid}`](/api-reference/videos/render-from-preview), passing the preview job ID in the path *** ## Recipe 10 — Poll Job Status **Use case:** You submitted a render and need to check whether it is done. **Endpoint:** `GET /v1/jobs/{jobid}` ```bash theme={null} curl --request GET \ --url 'https://api.pictory.ai/pictoryapis/v1/jobs/YOUR_JOB_ID' \ --header 'Authorization: YOUR_API_KEY' ``` **Response shape (when complete):** ```json theme={null} { "job_id": "9b1c4d2e-7f8a-4321-b2c3-d456e789f012", "success": true, "data": { "status": "completed", "progress": 100, "videoURL": "https://d3uryq9bhgb5qr.cloudfront.net/.../video.mp4", "videoShareURL": "https://video.pictory.ai/.../...", "videoEmbedURL": "https://video.pictory.ai/embed/.../...", "thumbnail": "https://d3uryq9bhgb5qr.cloudfront.net/.../thumbnail.jpg", "videoDuration": 65.6, "aiCreditsUsed": 48 } } ``` The rendered video URL is at `data.videoURL`. Poll every 10–30 seconds. Typical render time is 2–10 minutes depending on video length and complexity. For full response details, see the [Get Video Render Job API](/api-reference/jobs/get-video-render-job-by-id). *** ## How to Choose the Right Recipe | Your Goal | Use Recipe | | --------------------------------- | ---------- | | Quick text-to-video | 1 | | Localize for non-English audience | 2 | | Convert PPT deck | 3 | | Avatar-narrated walk-through | 4 | | Reuse a master template | 5 | | Re-render a saved project | 6 | | Apply brand kit | 7 | | Avoid polling | 8 | | Review before committing render | 9 | | Check job status | 10 | *** ## Next Steps Hand these recipes to an LLM for natural-language video generation Full endpoint-level documentation Machine-readable spec for codegen and tooling Async result delivery via webhook # Smart Layouts for Videos Source: https://docs.pictory.ai/guides/smart-layouts-and-subtitles/smart-layouts Create professional videos with smart layouts using the Pictory API for enhanced visual presentation This guide shows you how to create videos with smart layouts that enhance the visual presentation of your content. Smart layouts provide professionally designed templates that combine text, visuals, and animations for engaging video content. Smart Layout Options ## What You'll Learn Apply professionally designed visual layouts Choose layouts by name or ID Create consistent, polished video aesthetics Use layouts with voice-over and other features ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Text content ready for video conversion * Understanding of which layout style fits your content ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Available Smart Layouts Pictory provides five distinct smart layout styles, each designed for different content types and visual aesthetics: | Layout Name | Style | Best Used For | | --------------------- | ----------------------------------------------------- | ----------------------------------------------------------------- | | **Modern minimalist** | Clean, professional design with subtle animations | Business presentations, corporate content, professional tutorials | | **Kinetic** | Dynamic, energetic layout with bold typography | Marketing videos, product launches, social media ads | | **Chic** | Elegant, sophisticated design with stylish elements | Fashion, lifestyle content, premium brand videos | | **Wanderlust** | Travel-inspired layout with split-screen visuals | Travel content, testimonials, storytelling videos | | **Bulletin** | News-style layout with structured information display | Educational content, news updates, informational videos | Smart layouts automatically handle text positioning, animations, and visual styling. When using smart layouts, the `maxSubtitleLines` parameter is not available at the scene level as the layout manages text display automatically. ## How Smart Layouts Work When you apply a smart layout to your video: 1. **Layout Selection** - You specify the layout by name or ID 2. **Text Processing** - Your story content is analyzed and formatted 3. **Visual Styling** - Layout-specific styling is applied to text and elements 4. **Animation Application** - Pre-designed animations enhance visual appeal 5. **Scene Composition** - Text, visuals, and backgrounds are combined 6. **Video Rendering** - Final video is assembled with the complete layout design ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createVideoWithSmartLayout() { try { console.log("Creating video with smart layout..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_smart_layout", // Smart layout configuration smartLayoutName: "modern minimalist", // Choose your layout // Voice-over configuration voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", // AI voice name speed: 100, // Normal speed (50-200) amplificationLevel: 0, // Normal volume (-1 to 1) }, ], }, // Scene configuration scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with smart layout is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithSmartLayout(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_video_with_smart_layout(): try: print("Creating video with smart layout...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_smart_layout', # Smart layout configuration 'smartLayoutName': 'modern minimalist', # Choose your layout # Voice-over configuration 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', # AI voice name 'speed': 100, # Normal speed (50-200) 'amplificationLevel': 0 # Normal volume (-1 to 1) } ] }, # Scene configuration 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with smart layout is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_smart_layout() ``` ## Understanding the Parameters ### Smart Layout Configuration | Parameter | Type | Required | Description | | ----------------- | ------ | -------- | -------------------------------------------------------------- | | `smartLayoutName` | string | No | Name of the smart layout (use this OR smartLayoutId, not both) | | `smartLayoutId` | string | No | ID of the smart layout (use this OR smartLayoutName, not both) | ### Valid Smart Layout Names | Value | Description | | ------------------- | ----------------------------------------------------- | | `modern minimalist` | Clean, professional design with subtle animations | | `kinetic` | Dynamic, energetic layout with bold typography | | `chic` | Elegant, sophisticated design with stylish elements | | `wanderlust` | Travel-inspired layout with split-screen visuals | | `bulletin` | News-style layout with structured information display | **Smart Layout Restrictions** When using smart layouts, the following restrictions apply: * `maxSubtitleLines` cannot be used at the scene level * The layout automatically manages text display and positioning * Use either `smartLayoutName` OR `smartLayoutId`, not both ## Layout Selection Methods ### Using Layout Name The simplest way to select a smart layout is by name: ```javascript theme={null} { smartLayoutName: "kinetic" } ``` ### Using Layout ID For programmatic consistency, you can use the layout ID: ```javascript theme={null} { smartLayoutId: "your-layout-id-here" } ``` Layout IDs provide stability if layout names change in the future. Use the name for human-readable code and ID for automated systems. ## Common Use Cases ### Corporate Presentation ```javascript theme={null} { smartLayoutName: "modern minimalist", voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 95, amplificationLevel: 0 }] }, scenes: [{ story: "Your quarterly results presentation content..." }] } ``` **Result:** Professional, clean video perfect for business presentations. ### Marketing Campaign ```javascript theme={null} { smartLayoutName: "kinetic", voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", speed: 110, amplificationLevel: 0.2 }] }, scenes: [{ story: "Introducing our latest product innovation..." }] } ``` **Result:** Dynamic, energetic video for product launches and promotions. ### Fashion and Lifestyle ```javascript theme={null} { smartLayoutName: "chic", voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 100, amplificationLevel: 0 }] }, scenes: [{ story: "Discover the latest trends in sustainable fashion..." }] } ``` **Result:** Elegant, sophisticated video for lifestyle and fashion brands. ### Travel Content ```javascript theme={null} { smartLayoutName: "wanderlust", voiceOver: { enabled: true, aiVoices: [{ speaker: "Amy", speed: 100, amplificationLevel: 0 }] }, scenes: [{ story: "Experience the breathtaking views of the Swiss Alps..." }] } ``` **Result:** Immersive, travel-inspired video with testimonial-style presentation. ### Educational Content ```javascript theme={null} { smartLayoutName: "bulletin", voiceOver: { enabled: true, aiVoices: [{ speaker: "Joanna", speed: 90, amplificationLevel: 0 }] }, scenes: [{ story: "Today we'll explore the fundamentals of machine learning..." }] } ``` **Result:** Structured, informational video perfect for tutorials and courses. ## Best Practices Choose the right layout for your content: * **Modern Minimalist:** Business reports, corporate updates, professional tutorials * **Kinetic:** Product launches, social media ads, energetic promotions * **Chic:** Fashion content, luxury brands, lifestyle videos * **Wanderlust:** Travel vlogs, testimonials, storytelling * **Bulletin:** Educational content, news updates, informational videos Smart layouts work best when paired with AI voice-over: * Voice narration enhances the visual storytelling * Text and voice complement each other * Creates more engaging, professional content * Voice speed should match the layout energy Select layouts based on viewer expectations: * **B2B Content:** Modern Minimalist, Bulletin * **Consumer Marketing:** Kinetic, Chic * **Social Media:** Kinetic, Wanderlust * **Educational:** Bulletin, Modern Minimalist * **Brand Storytelling:** Wanderlust, Chic ## Troubleshooting **Problem:** API returns error about invalid layout name. **Solution:** * Check spelling and case sensitivity * Use exact names: "modern minimalist", "kinetic", "chic", "wanderlust", "bulletin" * Ensure you are not using both name and ID * Remove extra spaces or characters from the name **Problem:** API returns error about maxSubtitleLines not being allowed. **Solution:** * Remove `maxSubtitleLines` from all scene configurations * Smart layouts automatically manage text display * The layout handles subtitle line formatting * Use scene breaks to control text pacing instead **Problem:** Video uses a different layout than expected. **Solution:** * Verify the layout name spelling is correct * Double-check you are not using both name and ID * Ensure the parameter is at the video level, not scene level * Review the available layout options in this guide ## Next Steps Enhance your smart layout videos with these features: Add professional narration to complement layouts Add music to enhance the visual experience Apply consistent branding across videos Add your logo to branded content ## API Reference For complete technical details, see: List all available smart layout templates Apply smart layouts to rendered videos Preview videos with smart layouts Monitor storyboard creation progress # AI Story Generation with Story CoPilot Source: https://docs.pictory.ai/guides/story-copilot/ai-story-generation Generate video scripts automatically using AI-powered Story CoPilot in the Pictory API This guide shows you how to use the Story CoPilot feature to automatically generate video scripts from a simple prompt. Instead of writing your own story content, let AI create engaging, platform-optimized scripts tailored to your video type and tone preferences. ## What You'll Learn Generate video scripts from simple prompts Create content optimized for different video types Tailor scripts for specific social platforms Adjust the voice and style of generated content ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * A clear idea of your video topic * Understanding of your target audience and platform ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Story CoPilot Works When you use Story CoPilot to generate a video: 1. **Prompt Submission** - You provide a topic or description for your video 2. **AI Processing** - The CoPilot AI analyzes your prompt along with video type, platform, and tone settings 3. **Script Generation** - AI generates an optimized script tailored to your specifications 4. **Scene Creation** - The generated script is split into scenes based on your settings 5. **Visual Selection** - AI selects appropriate stock visuals for each scene 6. **Video Rendering** - Final video is assembled with voiceover, visuals, and captions Story CoPilot generates content based on your prompt and settings. The AI considers video type, target platform, and tone to create contextually appropriate scripts that engage your intended audience. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithStoryCoPilot() { try { console.log("Creating video with AI-generated script..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "ai_generated_video", // Voice-over configuration voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 100, amplificationLevel: 0, }, ], }, // Scene with Story CoPilot scenes: [ { // Story CoPilot configuration storyCoPilot: { prompt: "How artificial intelligence is transforming video creation and making professional content accessible to everyone", videoType: "Explainer", // Type of video duration: 60, // Target duration in seconds platform: "YouTube", // Target platform tone: "informative", // Content tone }, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with AI-generated script is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithStoryCoPilot(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_story_copilot(): try: print("Creating video with AI-generated script...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'ai_generated_video', # Voice-over configuration 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 100, 'amplificationLevel': 0 } ] }, # Scene with Story CoPilot 'scenes': [ { # Story CoPilot configuration 'storyCoPilot': { 'prompt': 'How artificial intelligence is transforming video creation and making professional content accessible to everyone', 'videoType': 'Explainer', # Type of video 'duration': 180, # Target duration in seconds 'platform': 'YouTube', # Target platform 'tone': 'informative' # Content tone }, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with AI-generated script is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_with_story_copilot() ``` ## Understanding the Parameters ### Story CoPilot Configuration | Parameter | Type | Required | Description | | ------------------------ | ------ | -------- | ----------------------------------------------------------------------- | | `storyCoPilot.prompt` | string | Yes | The topic or description for AI to generate content (1-5000 characters) | | `storyCoPilot.videoType` | string | No | Type of video to create (default: "Explainer") | | `storyCoPilot.duration` | number | No | Target video duration in seconds (1-600) | | `storyCoPilot.platform` | string | No | Target social media platform for optimization | | `storyCoPilot.tone` | string | No | Tone and style of the generated content | ### Video Types | Value | Description | Best Used For | | ------------------------ | -------------------------------------------------- | -------------------------------------------------------- | | `Explainer` | Educational content that explains concepts clearly | How-to videos, concept explanations, educational content | | `Marketing` | Promotional content designed to engage and convert | Product promotions, brand awareness, advertising | | `Internal Communication` | Professional content for internal teams | Company updates, training videos, team announcements | | `Tutorial` | Step-by-step instructional content | Software tutorials, DIY guides, learning materials | | `Product` | Product-focused content highlighting features | Product demos, feature showcases, launch videos | ### Target Platforms | Value | Optimization | Typical Duration | | ----------- | -------------------------------------- | ---------------- | | `YouTube` | Longer-form, detailed content | 2-10 minutes | | `TikTok` | Short, punchy, trend-aware content | 15-60 seconds | | `Instagram` | Visual-first, engaging content | 30-90 seconds | | `Facebook` | Shareable, social-friendly content | 1-3 minutes | | `LinkedIn` | Professional, business-focused content | 1-3 minutes | | `Twitter` | Concise, attention-grabbing content | 15-45 seconds | ### Content Tones | Value | Style | Best Used For | | ---------------- | ---------------------------- | ---------------------------------- | | `professional` | Formal, business-appropriate | Corporate content, B2B videos | | `casual` | Relaxed, everyday language | Social media, lifestyle content | | `friendly` | Warm, approachable | Customer-facing content, tutorials | | `informative` | Clear, fact-focused | Educational content, explainers | | `persuasive` | Compelling, action-oriented | Marketing, sales videos | | `exciting` | Energetic, enthusiastic | Product launches, announcements | | `educational` | Teaching-focused, structured | Training, learning materials | | `humorous` | Light-hearted, entertaining | Social media, brand personality | | `serious` | Grave, important | Announcements, compliance content | | `conversational` | Natural, dialogue-like | Vlogs, personal content | **Important Considerations** * Story CoPilot requires a valid prompt (1-5000 characters) * Cannot be used together with `story`, `blogUrl`, `pptUrl`, `audioUrl`, or `videoUrl` in the same scene * The generated content quality depends on the clarity and specificity of your prompt * Duration is a target guide - actual length may vary based on generated content ## Common Use Cases ### YouTube Explainer Video ```javascript theme={null} { scenes: [{ storyCoPilot: { prompt: "The benefits of renewable energy sources and how they're changing the future of power generation", videoType: "Explainer", duration: 180, platform: "YouTube", tone: "informative" }, createSceneOnEndOfSentence: true }] } ``` ### TikTok Marketing Content ```javascript theme={null} { scenes: [{ storyCoPilot: { prompt: "Why our new fitness app helps you achieve your workout goals faster than any other app", videoType: "Marketing", duration: 30, platform: "TikTok", tone: "exciting" }, createSceneOnEndOfSentence: true }] } ``` ### LinkedIn Professional Update ```javascript theme={null} { scenes: [{ storyCoPilot: { prompt: "How remote work is reshaping corporate culture and what leaders need to know", videoType: "Internal Communication", duration: 90, platform: "LinkedIn", tone: "professional" }, createSceneOnEndOfSentence: true }] } ``` ### Instagram Product Showcase ```javascript theme={null} { scenes: [{ storyCoPilot: { prompt: "Introducing our new eco-friendly water bottle with temperature control technology", videoType: "Product", duration: 45, platform: "Instagram", tone: "friendly" }, createSceneOnEndOfSentence: true }] } ``` ### Tutorial Video ```javascript theme={null} { scenes: [{ storyCoPilot: { prompt: "Step-by-step guide to setting up a home office for maximum productivity", videoType: "Tutorial", duration: 120, platform: "YouTube", tone: "educational" }, createSceneOnEndOfSentence: true }] } ``` ## Combining with Other Features ### With Custom Voice Settings ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 95, amplificationLevel: 0 }] }, scenes: [{ storyCoPilot: { prompt: "Your topic here...", videoType: "Explainer", tone: "friendly" }, createSceneOnEndOfSentence: true }] } ``` ### With Background Music ```javascript theme={null} { backgroundMusic: { enabled: true, autoMusic: true, volume: 0.3 }, voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 100 }] }, scenes: [{ storyCoPilot: { prompt: "Your topic here...", videoType: "Marketing", tone: "exciting" }, createSceneOnEndOfSentence: true }] } ``` ### With Brand Settings ```javascript theme={null} { brandName: "Your Brand Name", voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", speed: 100 }] }, scenes: [{ storyCoPilot: { prompt: "Your topic here...", videoType: "Marketing", platform: "LinkedIn", tone: "professional" }, createSceneOnEndOfSentence: true }] } ``` ### With Subtitle Styling ```javascript theme={null} { subtitleStyleName: "Corporate Blue", voiceOver: { enabled: true, aiVoices: [{ speaker: "Joanna", speed: 100 }] }, scenes: [{ storyCoPilot: { prompt: "Your topic here...", videoType: "Internal Communication", tone: "professional" }, createSceneOnEndOfSentence: true }] } ``` ## Writing Effective Prompts More detail in your prompt leads to better results: **Less Effective:** ``` "Talk about AI" ``` **More Effective:** ``` "Explain how artificial intelligence is being used in healthcare to improve patient diagnosis accuracy and reduce waiting times in emergency rooms" ``` Mention specific points you want covered: **Example:** ``` "Create a video about electric vehicles covering: environmental benefits, cost savings on fuel, available tax incentives, and the growing charging infrastructure" ``` Help the AI understand who you are speaking to: **Example:** ``` "Explain blockchain technology for small business owners who have no technical background but want to understand how it could benefit their operations" ``` If you want viewers to take action, mention it: **Example:** ``` "Promote our new project management software, highlighting the time-saving features and encouraging viewers to sign up for a free trial" ``` Provide background information when helpful: **Example:** ``` "Given the recent rise in remote work, explain how team collaboration tools have evolved and what features modern teams should look for in 2024" ``` ## Best Practices Choose the video type that best fits your goal: * **Explainer:** When educating or informing * **Marketing:** When promoting or selling * **Tutorial:** When teaching step-by-step processes * **Product:** When showcasing features and benefits * **Internal Communication:** For company/team content Set the platform to get content optimized for that audience: * **TikTok:** Shorter, trend-aware, attention-grabbing * **YouTube:** More detailed, structured, SEO-friendly * **LinkedIn:** Professional, business-focused language * **Instagram:** Visual-first, engaging hooks Match tone to your brand and audience: * **B2B Content:** Professional, informative * **Consumer Marketing:** Friendly, exciting, persuasive * **Educational:** Informative, educational * **Social Media:** Casual, conversational, humorous Guide the AI with appropriate target lengths: * Short social content: 15-60 seconds * Standard videos: 60-180 seconds * Detailed content: 180-600 seconds * The AI will adjust content density accordingly Story CoPilot is a starting point: * Review the generated content * Create variations with different prompts * Adjust tone or platform settings for different results * Use the best output as your final video ## Troubleshooting **Problem:** The AI-generated script lacks specificity. **Solution:** * Make your prompt more detailed and specific * Include key points you want covered * Mention your target audience * Add context or background information * Specify industry or domain terminology **Problem:** The generated script does not feel right for your brand. **Solution:** * Try a different tone setting * Adjust the videoType to better match your needs * Include tone guidance in your prompt * Combine tones (e.g., "professional yet friendly") * Test multiple tone options to find the best fit **Problem:** Generated content does not match target duration. **Solution:** * Adjust the `duration` parameter * Note that duration is a target, not exact * Longer prompts may generate longer content * Shorter prompts may generate shorter content * Platform setting also influences content length **Problem:** API returns error about conflicting parameters. **Solution:** * Remove the `story` parameter when using `storyCoPilot` * Only one content source allowed per scene * Choose either: storyCoPilot, story, blogUrl, pptUrl, audioUrl, or videoUrl * Each scene can only have one content source **Problem:** Generated script does not address your intended topic. **Solution:** * Rephrase your prompt more clearly * Start with the main topic explicitly stated * Remove ambiguous wording * Be direct about what you want covered * Check for typos or unclear references ## Next Steps Enhance your AI-generated videos with these features: Add professional narration to your videos Add music to complement your content Apply consistent branding across videos Use pre-designed layouts for professional presentation ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress * [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) - List available AI voices # Create Template from Project Source: https://docs.pictory.ai/guides/template-to-video/create-template-from-project Create reusable video templates from Pictory projects with advanced scene manipulation, layer overrides, and dynamic content insertion This guide covers an advanced use-case where you design a project with well-defined layouts, subtitles, header text, and layered images or videos in the Pictory App, then use the API to create a reusable template. You can dynamically replace text, images, and videos at runtime to create new videos based on your project's design. ## What You'll Learn Convert Pictory projects into reusable API templates Use placeholder variables for personalization at scale Modify existing scene content including subtitles and layers Insert, replace, or append new scenes to templates Replace text, images, and videos programmatically Use variables and overrides together for maximum flexibility ## Before You Begin Make sure you have: * A Pictory account ([create one here](https://app.pictory.ai)) * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * A project created in Pictory with your desired design and layout * Node.js or Python installed on your machine ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Workflow Overview ```mermaid theme={null} flowchart LR A[Create Project in Pictory App] --> B[Download .pictai File] B --> C[Upload to Template API] C --> D[Get Template Details] D --> E[Create Videos with Overrides] ``` ## Step-by-Step Guide ### Step 1: Create a Project in Pictory App Design your video project in the [Pictory App](https://app.pictory.ai) with all the elements you want to reuse: 1. Open [app.pictory.ai](https://app.pictory.ai) and create a new project 2. Design your scenes with: * **Background visuals** (images or videos) * **Text layers** (headers, titles, captions) * **Subtitles** for voice-over narration * **Layouts** that define visual composition 3. Add template variables using `{{variableName}}` syntax for dynamic content 4. Preview and finalize your project design Pictory storyboard editor with scenes showing layered text, background visuals, and subtitles The example project "art of mindfulness" contains 4 scenes with: * Background videos and images * Header text layers ("Present Moment Awareness", "The Art of Mindfulness") * Subtitles that will be narrated as voice-over * Custom layouts and styling **Design Tips for Templates:** * Use consistent layouts across scenes for professional results * Add placeholder text using `{{variables}}` for dynamic personalization * Test your design with different content lengths to ensure proper fitting ### Step 2: Download the Project File Once your project is ready, download it as a `.pictai` file: 1. Go to your [Projects Page](https://app.pictory.ai/myvideos) 2. Find your project and click on it to open details 3. Click the download icon next to the project file 4. Save the `.pictai` file to your computer Pictory projects page with download button highlighted for the project file The `.pictai` file contains your complete project configuration including scenes, layers, audio settings, and visual assets. ### Step 3: Create a Template from the Project Upload your `.pictai` file to the Template API to create a reusable template: ```http theme={null} POST https://api.pictory.ai/pictoryapis/v1/templates Content-Type: application/octet-stream Authorization: YOUR_API_KEY ``` ```javascript Node.js theme={null} import axios from "axios"; import fs from "fs"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createTemplateFromProject(filePath) { console.log("Creating template from project file..."); const fileBuffer = fs.readFileSync(filePath); const response = await axios.post( `${API_BASE_URL}/v1/templates`, fileBuffer, { headers: { "Content-Type": "application/octet-stream", Authorization: API_KEY, }, } ); const template = response.data; console.log("Template created successfully!"); console.log("Template ID:", template.templateId); console.log("Template Name:", template.name); console.log("Scenes:", template.scenes.length); return template; } // Upload your .pictai file createTemplateFromProject("art of mindfulness.pictai") .then(template => console.log("\nTemplate ready for use!")) .catch(error => console.error("Error:", error.message)); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_template_from_project(file_path): print("Creating template from project file...") with open(file_path, 'rb') as file: response = requests.post( f'{API_BASE_URL}/v1/templates', data=file, headers={ 'Content-Type': 'application/octet-stream', 'Authorization': API_KEY } ) response.raise_for_status() template = response.json() print("Template created successfully!") print(f"Template ID: {template['templateId']}") print(f"Template Name: {template['name']}") print(f"Scenes: {len(template['scenes'])}") return template # Upload your .pictai file template = create_template_from_project('art of mindfulness.pictai') print("\nTemplate ready for use!") ``` ```bash cURL theme={null} curl --location 'https://api.pictory.ai/pictoryapis/v1/templates' \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/octet-stream' \ --data-binary '@art of mindfulness.pictai' ``` ### Step 4: Get Template Details Retrieve your template details to see the structure, scene IDs, layer IDs, and available customization points: ```http theme={null} GET https://api.pictory.ai/pictoryapis/v1/templates/{templateId} Authorization: YOUR_API_KEY ``` ```javascript Node.js theme={null} async function getTemplateDetails(templateId) { const response = await axios.get( `${API_BASE_URL}/v1/templates/${templateId}`, { headers: { Authorization: API_KEY }, } ); const template = response.data; console.log("Template Details:"); console.log("Name:", template.name); console.log("Language:", template.language); console.log("\nScenes:"); template.scenes.forEach((scene, index) => { console.log(`\n Scene ${index + 1} (ID: ${scene.sceneId})`); // Show subtitles scene.subtitles?.forEach(sub => { console.log(` Subtitle: "${sub.text.substring(0, 50)}..."`); }); // Show layers scene.layers?.flat().forEach(layer => { console.log(` Layer (${layer.type}): ${layer.layerId}`); if (layer.text) console.log(` Text: "${layer.text}"`); }); }); return template; } getTemplateDetails("20260109004915031f181efb6b65749d7b5eae230cdc9c5df"); ``` ```python Python theme={null} def get_template_details(template_id): response = requests.get( f'{API_BASE_URL}/v1/templates/{template_id}', headers={'Authorization': API_KEY} ) response.raise_for_status() template = response.json() print("Template Details:") print(f"Name: {template['name']}") print(f"Language: {template['language']}") print("\nScenes:") for index, scene in enumerate(template['scenes']): print(f"\n Scene {index + 1} (ID: {scene['sceneId']})") # Show subtitles for sub in scene.get('subtitles', []): print(f" Subtitle: \"{sub['text'][:50]}...\"") # Show layers for layer_group in scene.get('layers', []): for layer in layer_group: print(f" Layer ({layer['type']}): {layer['layerId']}") if layer.get('text'): print(f" Text: \"{layer['text']}\"") return template get_template_details('20260109004915031f181efb6b65749d7b5eae230cdc9c5df') ``` **Example Template Response:** ```json theme={null} { "templateId": "20260109004915031f181efb6b65749d7b5eae230cdc9c5df", "name": "art of mindfulness", "language": "en", "published": true, "backgroundMusic": { "enabled": true, "musicUrl": "https://tracks.melod.ie/track_versions/...", "volume": 1 }, "scenes": [ { "sceneId": "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1", "subtitles": [ { "text": "The art of mindfulness is the practice of paying full attention to the present moment with calmness and without judgment." } ], "layers": [ [ { "layerId": "20260109004557054ef9462b6ae5f4d81afdafeb85ed369f1", "type": "video", "url": "https://media.gettyimages.com/..." }, { "layerId": "202601090045570540d50b1852611498aa9f51b69131648ad", "type": "text", "text": "Present Moment Awareness" }, { "layerId": "2026010900455705400270d33493b471d9b7e722bd804f06a", "type": "text", "text": "The Art of Mindfulness" } ] ] }, { "sceneId": "20260109004556808ff7ed6167ea1471e8bc034c4844bb56d", "subtitles": [ { "text": "It helps you notice your thoughts, feelings, and surroundings..." } ] } ] } ``` ## Using Template Variables Template variables provide a simple way to personalize videos without modifying the underlying structure. Variables defined in your project using `{{variableName}}` syntax are automatically detected and can be replaced when rendering videos. ### How Template Variables Work When you create a template from a project that contains `{{variableName}}` placeholders, these become customizable variables. The template response includes a `variables` object showing all detected placeholders. ### Variable Syntax | Syntax | Description | | ----------------- | ------------------------------------- | | `{{Name}}` | Simple variable for personalization | | `{{CompanyName}}` | Variable for company-specific content | | `{{ProductName}}` | Variable for product names | | `{{Date}}` | Variable for dynamic dates | Variables can be placed in: * Scene subtitles/narration text * Text layers (headers, titles, captions) * Any text element in your project ### Example: Template with Variables If your template contains text like: ``` Scene 1: "Welcome {{Name}} to {{CompanyName}}!" Scene 2: "Your personalized journey starts on {{Date}}." ``` The template response will include: ```json theme={null} { "templateId": "your_template_id", "name": "Welcome Template", "variables": { "Name": "NAME", "CompanyName": "COMPANY_NAME", "Date": "DATE" } } ``` ### Render Video with Variable Substitution Use the `variables` object to replace placeholders with actual values: ```javascript Node.js theme={null} const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: "your_template_id", videoName: "welcome_john_acme", variables: { Name: "John Smith", CompanyName: "Acme Corporation", Date: "January 15, 2026" } }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); console.log("Job ID:", response.data.data.jobId); ``` ```python Python theme={null} response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': 'your_template_id', 'videoName': 'welcome_john_acme', 'variables': { 'Name': 'John Smith', 'CompanyName': 'Acme Corporation', 'Date': 'January 15, 2026' } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) print(f"Job ID: {response.json()['data']['jobId']}") ``` This transforms: * `{{Name}}` → `John Smith` * `{{CompanyName}}` → `Acme Corporation` * `{{Date}}` → `January 15, 2026` ### Generating Personalized Videos at Scale The real power of template variables is generating personalized videos for many recipients: ```javascript Node.js theme={null} const recipients = [ { name: "John Smith", company: "Acme Corp", date: "January 15" }, { name: "Sarah Wilson", company: "Tech Inc", date: "January 16" }, { name: "Michael Brown", company: "Global Ltd", date: "January 17" }, ]; async function createPersonalizedVideos() { const jobs = []; for (const recipient of recipients) { console.log(`Creating video for ${recipient.name}...`); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: TEMPLATE_ID, videoName: `welcome_${recipient.name.toLowerCase().replace(" ", "_")}`, variables: { Name: recipient.name, CompanyName: recipient.company, Date: recipient.date } }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); jobs.push({ recipient: recipient.name, jobId: response.data.data.jobId }); console.log(`Started job: ${response.data.data.jobId}`); } return jobs; } createPersonalizedVideos(); ``` ```python Python theme={null} recipients = [ {'name': 'John Smith', 'company': 'Acme Corp', 'date': 'January 15'}, {'name': 'Sarah Wilson', 'company': 'Tech Inc', 'date': 'January 16'}, {'name': 'Michael Brown', 'company': 'Global Ltd', 'date': 'January 17'}, ] def create_personalized_videos(): jobs = [] for recipient in recipients: print(f"Creating video for {recipient['name']}...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': TEMPLATE_ID, 'videoName': f"welcome_{recipient['name'].lower().replace(' ', '_')}", 'variables': { 'Name': recipient['name'], 'CompanyName': recipient['company'], 'Date': recipient['date'] } }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY} ) response.raise_for_status() job_id = response.json()['data']['jobId'] jobs.append({'recipient': recipient['name'], 'jobId': job_id}) print(f"Started job: {job_id}") return jobs create_personalized_videos() ``` ### Common Variable Use Cases **Personalized Greetings:** ```json theme={null} { "templateId": "greeting_template_id", "videoName": "birthday_greeting", "variables": { "RecipientName": "John", "Occasion": "Birthday", "SenderName": "The Marketing Team" } } ``` **Product Promotions:** ```json theme={null} { "templateId": "promo_template_id", "videoName": "winter_sale", "variables": { "ProductName": "Winter Collection", "Discount": "25%", "PromoCode": "WINTER25" } } ``` **Customer Onboarding:** ```json theme={null} { "templateId": "onboarding_template_id", "videoName": "onboarding_acme", "variables": { "CompanyName": "Acme Corp", "AccountManager": "Mike Wilson", "TrialDays": "14" } } ``` **Variable Naming Tips:** * Use descriptive names like `{{CustomerName}}` instead of `{{n}}` * Variables are case-sensitive: `{{Name}}` and `{{name}}` are different * Avoid spaces in variable names: use `{{FirstName}}` not `{{First Name}}` ### Combining Variables with Template Overrides You can use both `variables` for simple text substitution and `templateOverride` for structural changes in the same request: ```json theme={null} { "templateId": "your_template_id", "videoName": "combined_example", "variables": { "Name": "John Smith", "CompanyName": "Acme Corp" }, "scenes": [ { "templateOverride": { "scenePosition": 1, "layers": [ [ { "layerId": "header_layer_id", "type": "image", "url": "https://your-cdn.com/acme-logo.png" } ] ] } } ] } ``` This approach allows you to: * Replace text variables globally with the `variables` object * Override specific layers (like logos or background videos) with `templateOverride` * Add, delete, or modify scenes while maintaining personalization ## Using Template Overrides The `templateOverride` object allows you to customize existing scenes, add new scenes, or modify layers when creating videos from your template. ### Understanding Template Override Structure ```json theme={null} { "templateId": "your_template_id", "scenes": [ { "templateOverride": { // Reference existing scene by ID or position "sceneId": "existing_scene_id", // OR "scenePosition": 1, // 1-based position // Modify existing scene content "subtitles": [...], // Override subtitles "layers": [...], // Override layers // Scene operations "deleteScene": true, // Delete this scene "copyScene": true // Duplicate this scene } } ] } ``` ### Override Existing Scene Subtitles Replace the subtitle text of an existing scene: ```javascript Node.js theme={null} const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: "20260109004915031f181efb6b65749d7b5eae230cdc9c5df", videoName: "mindfulness_customized", scenes: [ { templateOverride: { sceneId: "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1", subtitles: [ { text: "Welcome to the journey of self-awareness and inner peace through mindfulness meditation." } ] } } ] }, { headers: { Authorization: API_KEY, "Content-Type": "application/json" } } ); ``` ```python Python theme={null} response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': '20260109004915031f181efb6b65749d7b5eae230cdc9c5df', 'videoName': 'mindfulness_customized', 'scenes': [ { 'templateOverride': { 'sceneId': '20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1', 'subtitles': [ { 'text': 'Welcome to the journey of self-awareness and inner peace through mindfulness meditation.' } ] } } ] }, headers={'Authorization': API_KEY, 'Content-Type': 'application/json'} ) ``` ### Override Scene Layers Replace text, images, or videos in scene layers: ```json theme={null} { "templateId": "your_template_id", "scenes": [ { "templateOverride": { "sceneId": "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1", "layers": [ [ { "layerId": "20260109004557054ef9462b6ae5f4d81afdafeb85ed369f1", "type": "video", "url": "https://your-cdn.com/new-background-video.mp4" }, { "layerId": "202601090045570540d50b1852611498aa9f51b69131648ad", "type": "text", "text": "New Header Text" }, { "layerId": "2026010900455705400270d33493b471d9b7e722bd804f06a", "type": "text", "text": "Updated Title" } ] ] } } ] } ``` ### Layer Override Options | Property | Type | Description | | ------------- | ------- | ----------------------------------------------- | | `layerId` | string | ID of the layer to modify | | `type` | string | Layer type: `text`, `image`, or `video` | | `text` | string | New text content (for text layers) | | `url` | string | New media URL (for image/video layers) | | `style` | object | Override text styling (font, color, size, etc.) | | `styleId` | string | Apply a predefined text style | | `styleName` | string | Apply a text style by name | | `deleteLayer` | boolean | Remove this layer from the scene | ### Override with Custom Text Styles ```json theme={null} { "templateOverride": { "sceneId": "your_scene_id", "layers": [ [ { "layerId": "your_layer_id", "type": "text", "text": "Styled Header", "style": { "fontFamily": "Montserrat", "fontSize": 48, "color": "#FF5722", "position": "top-center", "alignment": "center", "decorations": ["bold"] } } ] ] } } ``` ### Delete or Copy Scenes **Delete a scene:** ```json theme={null} { "templateOverride": { "sceneId": "scene_to_delete", "deleteScene": true } } ``` **Duplicate a scene:** ```json theme={null} { "templateOverride": { "scenePosition": 2, "copyScene": true } } ``` ## Adding New Scenes to Templates You can insert new scenes into your template at specific positions. New scenes require content from one of these sources: `story`, `storyCoPilot`, `blogUrl`, `pptUrl`, `audioUrl`, or `videoUrl`. ### Scene Positioning Options | Property | Description | | ---------------------- | ------------------------------------- | | `newScenePosition` | Insert at specific position (1-based) | | `insertAfterSceneId` | Insert after a specific scene ID | | `insertBeforeSceneId` | Insert before a specific scene ID | | `replaceSceneId` | Replace an existing scene by ID | | `replaceScenePosition` | Replace scene at position (1-based) | ### Inherit Style from Existing Scene When adding new scenes, you can inherit the layout and styling from an existing scene: | Property | Description | | ------------------- | ---------------------------------------- | | `baseSceneId` | Use this scene's style as the base | | `baseScenePosition` | Use scene at this position as style base | ### Example: Insert a New Scene After Scene 1 ```json theme={null} { "templateId": "20260109004915031f181efb6b65749d7b5eae230cdc9c5df", "videoName": "mindfulness_with_new_scene", "scenes": [ { "templateOverride": { "insertAfterSceneId": "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1", "baseSceneId": "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1" }, "story": "Mindfulness helps us connect with our inner self and find peace in everyday moments." } ] } ``` ### Example: Replace Scene 3 with New Content ```javascript Node.js theme={null} const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: "20260109004915031f181efb6b65749d7b5eae230cdc9c5df", videoName: "mindfulness_replaced_scene", scenes: [ { templateOverride: { replaceScenePosition: 3, baseScenePosition: 1 // Use Scene 1's style }, story: "Practice deep breathing: inhale for 4 counts, hold for 4, exhale for 4. This simple technique calms your nervous system instantly." } ] }, { headers: { Authorization: API_KEY, "Content-Type": "application/json" } } ); ``` ```python Python theme={null} response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': '20260109004915031f181efb6b65749d7b5eae230cdc9c5df', 'videoName': 'mindfulness_replaced_scene', 'scenes': [ { 'templateOverride': { 'replaceScenePosition': 3, 'baseScenePosition': 1 }, 'story': 'Practice deep breathing: inhale for 4 counts, hold for 4, exhale for 4. This simple technique calms your nervous system instantly.' } ] }, headers={'Authorization': API_KEY, 'Content-Type': 'application/json'} ) ``` ### Example: Add Scene at Specific Position ```json theme={null} { "templateId": "your_template_id", "scenes": [ { "templateOverride": { "newScenePosition": 2, "baseSceneId": "existing_scene_id" }, "story": "This new scene will appear at position 2, pushing existing scenes down." } ] } ``` ### New Scene Content Sources When adding new scenes, provide content using one of these properties: | Property | Description | | -------------- | --------------------------------------------- | | `story` | Plain text content for the scene narration | | `storyCoPilot` | AI-generated content based on a prompt | | `blogUrl` | URL to a blog article to extract content from | | `pptUrl` | URL to a PowerPoint presentation | | `audioUrl` | URL to an audio file for transcription | | `videoUrl` | URL to a video file for repurposing | **Example with Story CoPilot:** ```json theme={null} { "templateOverride": { "insertAfterSceneId": "scene_123", "baseSceneId": "scene_123" }, "storyCoPilot": { "videoType": "Explainer", "prompt": "Explain the benefits of morning meditation for productivity", "tone": "friendly", "duration": 30 } } ``` **Example with Blog URL:** ```json theme={null} { "templateOverride": { "newScenePosition": 3, "baseScenePosition": 1 }, "blogUrl": "https://example.com/article-about-mindfulness" } ``` ## Complete Example: Dynamic Video Generation Here's a complete example that demonstrates overriding existing content and adding new scenes: ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const TEMPLATE_ID = "20260109004915031f181efb6b65749d7b5eae230cdc9c5df"; async function createCustomizedVideo() { console.log("Creating customized video from template..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: TEMPLATE_ID, videoName: "mindfulness_personalized", // Override voice settings voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 100 }] }, scenes: [ // Override Scene 1: Update subtitles and text layers { templateOverride: { scenePosition: 1, subtitles: [ { text: "Welcome to your personal mindfulness journey. Let's explore the art of being present." } ], layers: [ [ { layerId: "202601090045570540d50b1852611498aa9f51b69131648ad", type: "text", text: "Your Journey Begins" }, { layerId: "2026010900455705400270d33493b471d9b7e722bd804f06a", type: "text", text: "Mindfulness for Beginners" } ] ] } }, // Add new scene after Scene 1 using AI-generated content { templateOverride: { insertAfterSceneId: "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1", baseSceneId: "20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1" }, storyCoPilot: { videoType: "Explainer", prompt: "Describe a simple 1-minute breathing exercise for stress relief", tone: "calm", duration: 15 } }, // Override Scene 2: Change subtitle text { templateOverride: { scenePosition: 2, subtitles: [ { text: "Notice your breath. Feel the air flowing in and out. This simple act anchors you to the present." } ] } }, // Replace Scene 4 with new story content { templateOverride: { replaceScenePosition: 4, baseScenePosition: 1 }, story: "As you continue this practice daily, you'll discover a deeper sense of calm and clarity in everything you do." } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("Video creation started!"); console.log("Job ID:", jobId); return jobId; } async function waitForVideo(jobId) { console.log("\nMonitoring video creation..."); while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const status = response.data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\nVideo is ready!"); console.log("Video URL:", response.data.data.videoURL); return response.data; } else if (status === "failed") { throw new Error("Video creation failed"); } await new Promise(resolve => setTimeout(resolve, 5000)); } } // Run the workflow createCustomizedVideo() .then(jobId => waitForVideo(jobId)) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' TEMPLATE_ID = '20260109004915031f181efb6b65749d7b5eae230cdc9c5df' def create_customized_video(): print("Creating customized video from template...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': TEMPLATE_ID, 'videoName': 'mindfulness_personalized', # Override voice settings 'voiceOver': { 'enabled': True, 'aiVoices': [{'speaker': 'Brian', 'speed': 100}] }, 'scenes': [ # Override Scene 1 { 'templateOverride': { 'scenePosition': 1, 'subtitles': [ {'text': 'Welcome to your personal mindfulness journey. Let\'s explore the art of being present.'} ], 'layers': [ [ { 'layerId': '202601090045570540d50b1852611498aa9f51b69131648ad', 'type': 'text', 'text': 'Your Journey Begins' }, { 'layerId': '2026010900455705400270d33493b471d9b7e722bd804f06a', 'type': 'text', 'text': 'Mindfulness for Beginners' } ] ] } }, # Add new scene after Scene 1 { 'templateOverride': { 'insertAfterSceneId': '20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1', 'baseSceneId': '20260109004556808c640d883dc9a4bfe983cba4eadb7a0f1' }, 'storyCoPilot': { 'videoType': 'Explainer', 'prompt': 'Describe a simple 1-minute breathing exercise for stress relief', 'tone': 'calm', 'duration': 15 } }, # Override Scene 2 { 'templateOverride': { 'scenePosition': 2, 'subtitles': [ {'text': 'Notice your breath. Feel the air flowing in and out. This simple act anchors you to the present.'} ] } }, # Replace Scene 4 { 'templateOverride': { 'replaceScenePosition': 4, 'baseScenePosition': 1 }, 'story': 'As you continue this practice daily, you\'ll discover a deeper sense of calm and clarity in everything you do.' } ] }, headers={'Content-Type': 'application/json', 'Authorization': API_KEY} ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("Video creation started!") print(f"Job ID: {job_id}") return job_id def wait_for_video(job_id): print("\nMonitoring video creation...") while True: response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) response.raise_for_status() status = response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': print("\nVideo is ready!") print(f"Video URL: {response.json()['data']['videoURL']}") return response.json() elif status == 'failed': raise Exception("Video creation failed") time.sleep(5) # Run the workflow if __name__ == '__main__': job_id = create_customized_video() wait_for_video(job_id) ``` ## Template Override Reference ### Referencing Existing Scenes | Property | Type | Description | | --------------- | ------ | ------------------------------------- | | `sceneId` | string | Reference scene by unique ID | | `scenePosition` | number | Reference scene by position (1-based) | Use either `sceneId` or `scenePosition`, not both. Scene IDs are more reliable as positions can change. ### Adding New Scenes | Property | Type | Description | | ---------------------- | ------ | --------------------------------- | | `newScenePosition` | number | Insert at this position (1-based) | | `insertAfterSceneId` | string | Insert after this scene | | `insertBeforeSceneId` | string | Insert before this scene | | `replaceSceneId` | string | Replace this scene entirely | | `replaceScenePosition` | number | Replace scene at this position | ### Styling New Scenes | Property | Type | Description | | ------------------- | ------ | ------------------------------------ | | `baseSceneId` | string | Inherit style from this scene | | `baseScenePosition` | number | Inherit style from scene at position | ### Scene Operations | Property | Type | Description | | ------------- | ------- | ----------------------------- | | `deleteScene` | boolean | Remove this scene from output | | `copyScene` | boolean | Duplicate this scene | ## Best Practices * Create modular scenes that work independently * Use consistent layer naming for easy identification * Design with various content lengths in mind * Test templates with different override combinations * Scene IDs are stable identifiers that do not change * Positions shift when scenes are added or removed * Store and reference scene IDs from the Get Template response * Use positions only when the template structure is fixed * Fetch template details to verify scene and layer IDs exist * Check that media URLs are accessible before submission * Test with small changes before complex overrides * Monitor job status and handle failures gracefully * Use optimized image and video formats * Host media on reliable CDNs with good latency * Match video dimensions to template aspect ratio * Keep file sizes reasonable for faster processing ## Troubleshooting **Problem:** API returns error about invalid scene or layer ID. **Solution:** * Fetch fresh template details using Get Template API * Verify the ID matches exactly (case-sensitive) * Check that the scene/layer has not been removed * Use `scenePosition` if scene IDs have changed **Problem:** Added scene does not show in rendered video. **Solution:** * Ensure you have provided a content source (story, blogUrl, etc.) * Verify positioning parameters are correct * Check that `baseSceneId` or `baseScenePosition` exists * Review job status for any processing errors **Problem:** Text or media changes not reflected in output. **Solution:** * Confirm the `layerId` matches template exactly * Ensure `type` matches the layer type in template * Check that media URLs are publicly accessible * Verify the layer is not being deleted elsewhere **Problem:** New scene does not match expected styling. **Solution:** * Verify `baseSceneId` or `baseScenePosition` is valid * Check that the base scene has the styling you expect * Only one of baseSceneId or baseScenePosition should be used * Ensure the base scene is not being deleted in the same request ## Next Steps Explore more advanced features to enhance your template-based workflows: Use variables for simple personalization at scale Add AI narration to your template videos Apply consistent layouts across scenes Maintain brand consistency in generated videos ## API Reference Upload .pictai files to create templates Retrieve template structure and IDs Create videos with template overrides Monitor video creation progress # Project as Template Source: https://docs.pictory.ai/guides/template-to-video/project-as-template Use your Pictory projects as templates to create personalized videos at scale with variable substitution This guide shows you how to use an existing Pictory project as a template to generate personalized videos programmatically. Create a project once in the Pictory App with placeholder variables, then use the API to generate unlimited variations by substituting different values. ## What You'll Learn Add placeholder variables to your project using double curly brackets Find the project ID from the Pictory App URL Use the API to render videos with variable substitution Generate personalized videos for different audiences ## Before You Begin Make sure you have: * A Pictory account with an existing project ([create one here](https://app.pictory.ai)) * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Basic understanding of API calls ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Template Variables Work Template variables are placeholders in your project that get replaced with actual values when you render the video. You define them using double curly brackets syntax: `{{variableName}}`. ### Variable Syntax | Syntax | Description | | ----------------- | ------------------------------------------------- | | `{{Name}}` | Simple variable that will be replaced with a name | | `{{Year}}` | Variable for dynamic year content | | `{{CompanyName}}` | Variable for company-specific content | | `{{ProductName}}` | Variable for product names | Variables can be placed in: * Scene subtitles/text * Any text element in your project ## Step-by-Step Guide ### Step 1: Create a Template Project in Pictory App 1. Open the [Pictory App](https://app.pictory.ai) and create a new project or open an existing one 2. In your scene text, add template variables using double curly brackets 3. Design your video with visuals, transitions, and any other elements you want **Example template text with variables:** ``` Scene 1: Wishing you a wonderful {{Year}}, {{Name}}! Scene 2: May this year bring you peace, growth, and beautiful memories. ``` Pictory project storyboard showing template variables in scene subtitles In this example, the project contains two template variables: * `{{Year}}` - Will be replaced with the target year (e.g., "2026") * `{{Name}}` - Will be replaced with the recipient's name (e.g., "James Thomas") **Variable Naming Tips:** * Use descriptive names like `{{CustomerName}}` instead of `{{n}}` * Variables are case-sensitive: `{{Name}}` and `{{name}}` are different * Avoid spaces in variable names: use `{{FirstName}}` not `{{First Name}}` ### Step 2: Get the Project ID Once your template project is ready, you need to get its project ID to use with the API. 1. Open your project in the Pictory App 2. Look at the browser address bar - the URL will look like: ``` https://app.pictory.ai/story/20260108050148221f72c80c778d14153b2d1628cf4f3cb72 ``` 3. The project ID is the long string after `/story/`: ``` 20260108050148221f72c80c778d14153b2d1628cf4f3cb72 ``` Browser address bar showing the project URL with project ID The project ID is a unique identifier for your project. Copy this ID exactly as shown — it is case-sensitive and must be complete. ### Step 3: Render Video from Template via API Now use the API to create videos from your template by providing the project ID as `templateId` and the variable values: ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Your template project ID from the Pictory App URL const TEMPLATE_ID = "20260108050148221f72c80c778d14153b2d1628cf4f3cb72"; async function createVideoFromTemplate() { try { console.log("Creating video from template..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: TEMPLATE_ID, videoName: "happy_new_year_james", variables: { Name: "James Thomas", Year: "2026" } }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error creating video:", error.response?.data || error.message); throw error; } } async function waitForVideo(jobId) { console.log("\nMonitoring video creation..."); while (true) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\n✓ Video is ready!"); console.log("Video URL:", statusResponse.data.data.videoURL); return statusResponse.data; } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } // Wait 5 seconds before checking again await new Promise(resolve => setTimeout(resolve, 5000)); } } // Run the complete workflow createVideoFromTemplate() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Replace with your actual API key # Your template project ID from the Pictory App URL TEMPLATE_ID = '20260108050148221f72c80c778d14153b2d1628cf4f3cb72' def create_video_from_template(): try: print("Creating video from template...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': TEMPLATE_ID, 'videoName': 'happy_new_year_james', 'variables': { 'Name': 'James Thomas', 'Year': '2026' } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error creating video: {error}") raise def wait_for_video(job_id): print("\nMonitoring video creation...") while True: response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) response.raise_for_status() status = response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': print("\n✓ Video is ready!") print(f"Video URL: {response.json()['data']['videoURL']}") return response.json() elif status == 'failed': raise Exception(f"Video creation failed: {response.json()}") # Wait 5 seconds before checking again time.sleep(5) # Run the complete workflow if __name__ == '__main__': job_id = create_video_from_template() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the API Request ### Request Parameters | Parameter | Type | Required | Description | | ------------ | ------ | -------- | -------------------------------------------------- | | `templateId` | string | Yes | The project ID from the Pictory App URL | | `name` | string | Yes | Name for the generated video | | `variables` | object | Yes | Key-value pairs for template variable substitution | ### Variables Object The `variables` object contains key-value pairs where: * **Key**: The variable name (without curly brackets) - must match exactly what you used in your template * **Value**: The string to replace the variable with ```json theme={null} { "variables": { "Name": "James Thomas", "Year": "2026" } } ``` This will transform: * `{{Name}}` → `James Thomas` * `{{Year}}` → `2026` ### Response ```json theme={null} { "success": true, "data": { "jobId": "9eb2bbbb-9e0b-42ff-81d7-f701094880b3" } } ``` ## Generating Personalized Videos at Scale The real power of template-based video creation is generating personalized videos for many recipients: ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const TEMPLATE_ID = "your_template_project_id"; // List of recipients for personalized videos const recipients = [ { name: "James Thomas", email: "james@example.com" }, { name: "Sarah Wilson", email: "sarah@example.com" }, { name: "Michael Brown", email: "michael@example.com" }, ]; async function createPersonalizedVideos() { const jobs = []; for (const recipient of recipients) { console.log(`Creating video for ${recipient.name}...`); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { templateId: TEMPLATE_ID, videoName: `new_year_greeting_${recipient.name.toLowerCase().replace(" ", "_")}`, variables: { Name: recipient.name, Year: "2026" } }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); jobs.push({ recipient: recipient, jobId: response.data.data.jobId }); console.log(`✓ Started job: ${response.data.data.jobId}`); } return jobs; } createPersonalizedVideos() .then(jobs => { console.log("\n✓ All video creation jobs started!"); console.log("Jobs:", jobs); }) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} import requests import time API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' TEMPLATE_ID = 'your_template_project_id' # List of recipients for personalized videos recipients = [ {'name': 'James Thomas', 'email': 'james@example.com'}, {'name': 'Sarah Wilson', 'email': 'sarah@example.com'}, {'name': 'Michael Brown', 'email': 'michael@example.com'}, ] def create_personalized_videos(): jobs = [] for recipient in recipients: print(f"Creating video for {recipient['name']}...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'templateId': TEMPLATE_ID, 'videoName': f"new_year_greeting_{recipient['name'].lower().replace(' ', '_')}", 'variables': { 'Name': recipient['name'], 'Year': '2026' } }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] jobs.append({ 'recipient': recipient, 'jobId': job_id }) print(f"✓ Started job: {job_id}") return jobs if __name__ == '__main__': jobs = create_personalized_videos() print("\n✓ All video creation jobs started!") print("Jobs:", jobs) ``` ## Common Use Cases ### Personalized Greetings ```json theme={null} { "templateId": "your_greeting_template_id", "videoName": "birthday_greeting_john", "variables": { "RecipientName": "John", "Occasion": "Birthday", "SenderName": "The Marketing Team" } } ``` ### Product Promotions ```json theme={null} { "templateId": "your_promo_template_id", "videoName": "product_promo_winter", "variables": { "ProductName": "Winter Collection", "Discount": "25%", "PromoCode": "WINTER25" } } ``` ### Event Invitations ```json theme={null} { "templateId": "your_event_template_id", "videoName": "webinar_invite_march", "variables": { "EventName": "AI in Marketing Webinar", "EventDate": "March 15, 2026", "SpeakerName": "Dr. Sarah Johnson" } } ``` ### Customer Onboarding ```json theme={null} { "templateId": "your_onboarding_template_id", "videoName": "onboarding_acme_corp", "variables": { "CompanyName": "Acme Corp", "AccountManager": "Mike Wilson", "TrialDays": "14" } } ``` ## Best Practices * Keep variable names generic and reusable * Use placeholders that make sense for your use case * Test your template with different variable values to ensure proper text fitting * Consider text length limits when designing scenes with variables * Document all variables used in your template * Use consistent naming conventions across templates * Validate variable values before sending to API * Handle special characters appropriately in variable values * Use webhooks for notification when videos complete * Process videos in batches for large campaigns * Store job IDs for tracking and retrieval * Implement retry logic for failed jobs * Create test videos with sample data before mass generation * Verify all variables are replaced correctly * Check video quality and timing with different text lengths * Confirm the output meets your quality standards ## Troubleshooting **Problem:** Template variable appears as `{{Name}}` instead of the actual value. **Solution:** * Ensure the variable name in your `variables` object matches exactly (case-sensitive) * Check for typos in the variable name * Verify the variable exists in the template project * Make sure there are no extra spaces in the variable name **Problem:** API returns an error about invalid template ID. **Solution:** * Verify you copied the complete project ID from the URL * Check that the project exists in your Pictory account * Ensure you are using the correct API key associated with the account * Confirm the project is accessible (not deleted or archived) **Problem:** Replaced variable text is too long and gets cut off. **Solution:** * Design templates with maximum expected text length in mind * Use shorter variable values or abbreviations * Adjust font size in the template to accommodate longer text * Consider using multiple scenes for longer content **Problem:** Video job remains in-progress for extended time. **Solution:** * Check the job status using the Get Job API * Verify your API subscription is active * Contact support if job is stuck for over 30 minutes * Retry with a new request if needed ## Next Steps Enhance your template-based workflows with these features: Add AI narration to your template videos Consistent brand styling across all generated videos Add background music to enhance your videos Save generated videos as editable projects ## API Reference For complete technical details, see: Create preview from template Render video directly from template Monitor video creation progress List your template projects # Text to Video with AI Voice-Over Source: https://docs.pictory.ai/guides/text-to-video/ai-voiceover Create videos from text with AI-generated voice-over narration using the Pictory API This guide shows you how to create videos from text with professional AI-generated voice-over narration. Transform written content into narrated videos perfect for social media, YouTube, educational content, or marketing materials. ## What You'll Learn Add natural-sounding narration to your videos Choose from various AI voice speakers Voice automatically syncs with video scenes Control speed and volume for perfect delivery ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Text content ready for video conversion * Basic understanding of voice-over concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Text-to-Video with Voice-Over Works When you create a video with AI voice-over: 1. **Text Processing** - Your text content is analyzed and prepared 2. **Scene Generation** - Text is split into logical video scenes 3. **Visual Selection** - AI selects appropriate stock visuals for each scene 4. **Voice Generation** - Professional AI narration is created from your text 5. **Synchronization** - Voice-over is automatically synchronized with video timing 6. **Caption Creation** - Subtitles are generated to match the narration 7. **Video Rendering** - Final video is assembled with all elements combined The AI voice-over is automatically synchronized with your video scenes. The narration timing adjusts based on text length, voice speed, and scene duration for natural pacing. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; const SAMPLE_TEXT = "AI is poised to significantly impact educators and course creators on social media. " + "By automating tasks like content generation, visual design, and video editing, " + "AI will save time and enhance consistency."; async function createTextToVideoWithVoiceOver() { try { console.log("Creating video with AI voice-over..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_with_ai_voice", // Voice-over configuration voiceOver: { enabled: true, // Enable voice-over aiVoices: [ { speaker: "Brian", // AI voice name speed: 100, // Normal speed (50-200) amplificationLevel: 0, // Normal volume (-1 to 1) }, ], }, // Scene configuration scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: true, // New scene per line createSceneOnEndOfSentence: true, // New scene per sentence }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with AI voice-over is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createTextToVideoWithVoiceOver(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' SAMPLE_TEXT = ( "AI is poised to significantly impact educators and course creators on social media. " "By automating tasks like content generation, visual design, and video editing, " "AI will save time and enhance consistency." ) def create_text_to_video_with_voiceover(): try: print("Creating video with AI voice-over...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_with_ai_voice', # Voice-over configuration 'voiceOver': { 'enabled': True, # Enable voice-over 'aiVoices': [ { 'speaker': 'Brian', # AI voice name 'speed': 100, # Normal speed (50-200) 'amplificationLevel': 0 # Normal volume (-1 to 1) } ] }, # Scene configuration 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': True, # New scene per line 'createSceneOnEndOfSentence': True # New scene per sentence } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with AI voice-over is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_text_to_video_with_voiceover() ``` ## Understanding the Parameters ### Voice-Over Configuration | Parameter | Type | Required | Description | | ----------------------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voiceOver.enabled` | boolean | Yes | Set to `true` to enable voice-over narration | | `voiceOver.aiVoices` | array | Yes | Array of AI voice configurations (currently supports one voice) | | `voiceOver.aiVoices[].speaker` | string | Yes | Name or voice ID of the AI voice. You can pass a voice name (e.g., `"Brian"`, `"Emma"`), a numeric track ID, or an ElevenLabs voice ID (e.g., `"pNInz6obpgDQGcFmaJgB"`). ElevenLabs voices are automatically discovered and added to your library if not already present. | | `voiceOver.aiVoices[].speed` | number | No | Voice speed 50-200 (default: 100 = normal) | | `voiceOver.aiVoices[].amplificationLevel` | number | No | Volume level -1 to 1 (default: 0 = normal) | ### Scene Configuration | Parameter | Type | Default | Description | | ---------------------------- | ------- | ------- | ------------------------------------ | | `scenes[].story` | string | - | Text content to be narrated | | `createSceneOnNewLine` | boolean | false | Create new scene at line breaks | | `createSceneOnEndOfSentence` | boolean | false | Create new scene at sentence endings | ## Voice Speed Reference | Speed Value | Playback Rate | Best Used For | | ----------- | -------------------------- | ------------------------------------------------- | | 50 | 0.5x (Very slow) | Complex technical content, learning materials | | 75 | 0.75x (Slower) | Detailed explanations, emphasis | | 90 | 0.9x (Slightly slower) | Professional presentations, important information | | 100 | 1.0x (Normal) | Standard content, most use cases | | 110-120 | 1.1-1.2x (Slightly faster) | Casual content, social media | | 150 | 1.5x (Fast) | Quick summaries, energetic content | | 200 | 2.0x (Very fast) | Speed reading, urgent updates | ## Amplification Level Reference | Level | Effect | Best Used For | | ----- | ------------------- | ----------------------------------- | | -1.0 | Quietest | Background narration, ambient voice | | -0.5 | Quieter than normal | De-emphasized content | | 0 | Normal volume | Standard narration, most content | | 0.3 | Slightly louder | Mild emphasis, important points | | 0.5 | Moderately louder | Strong emphasis | | 1.0 | Loudest | Maximum emphasis, calls-to-action | **Volume Considerations:** Very high amplification levels (0.7-1.0) may cause audio distortion. Test your settings and use moderation for professional results. ## Choosing Your AI Voice The Pictory API supports various AI voice speakers with different characteristics: | Voice Type | Example Names | Best Used For | | ------------------- | ---------------------- | --------------------------------------------------------- | | Male Professional | Brian, Matthew, Joey | Business content, technical tutorials, corporate videos | | Female Professional | Emma, Joanna, Amy | Educational content, friendly tutorials, customer service | | Conversational | Multiple options | Casual content, social media, storytelling | | ElevenLabs Premium | Pass voice ID directly | Ultra-realistic, expressive narration | To get a complete list of available voices programmatically, use the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) API endpoint. This returns all available AI voices with their names, languages, and characteristics. **ElevenLabs Auto-Discovery:** You can pass any ElevenLabs voice ID directly as the `speaker` value. If the voice is not already in your library, it will be automatically discovered and added from the ElevenLabs catalog. No manual setup required. See the [ElevenLabs Voices Guide](/guides/voice-over/elevenlabs-voices) for details. ## Common Use Cases ### Educational Content ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", // Clear female voice speed: 95, // Slightly slower for comprehension amplificationLevel: 0 }] } } ``` **Result:** Clear, paced narration perfect for learning materials. ### Marketing and Sales ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Matthew", // Professional male voice speed: 110, // Slightly faster for energy amplificationLevel: 0.2 // Louder for emphasis }] } } ``` **Result:** Dynamic, engaging narration for promotional content. ### Social Media Content ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Amy", // Friendly female voice speed: 115, // Faster pace for social amplificationLevel: 0.1 }] } } ``` **Result:** Energetic narration for short-form social videos. ### Technical Tutorials ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", // Professional male voice speed: 90, // Slower for technical content amplificationLevel: 0 }] } } ``` **Result:** Measured, clear narration for complex topics. ## Best Practices Match voice characteristics to your content: * **Professional Content**: Use Brian, Matthew, or Joanna * **Casual/Friendly**: Use Emma, Amy, or Joey * **Educational**: Choose clear voices like Emma or Brian * **Brand Consistency**: Use the same voice across all your videos * **Test Options**: Try different voices to find the best fit Adjust speed based on content type: * **Complex Content**: Use 80-95 for technical or educational material * **Standard Content**: Use 95-105 for most narration * **Social Media**: Use 110-120 for energetic, engaging delivery * **Urgent Content**: Use 120-150 for quick updates * **Test Pacing**: Always preview to ensure speed feels natural Apply volume strategically: * **Subtle Emphasis**: Use 0.1-0.3 for gentle highlighting * **Normal Content**: Keep at 0 for most narration * **Strong Emphasis**: Use 0.4-0.6 for key messages * **Avoid Extremes**: Don't exceed 0.7 to prevent distortion * **Consistency**: Maintain similar levels across similar content Prepare text that sounds natural when spoken: * **Conversational Tone**: Write as you would speak, not formal writing * **Short Sentences**: Keep sentences concise for better pacing * **Clear Pronunciation**: Spell out acronyms and difficult terms * **Proper Punctuation**: Use periods and commas for natural pauses * **Test by Reading**: Read your text aloud before converting Structure scenes for effective narration: * **Logical Breaks**: Split at natural pauses in your content * **Scene Length**: Aim for 5-15 seconds per scene (30-100 words) * **Visual Match**: Ensure each scene's visuals match the narration * **Pacing**: Use scene breaks to control video rhythm * **Test Flow**: Review to ensure smooth transitions ## Troubleshooting **Problem:** The AI narration does not sound natural. **Solution:** * Try a different AI voice speaker * Adjust speed to 95-105 for more natural cadence * Ensure your text has proper punctuation * Avoid using all caps or unusual formatting * Write in a conversational tone, not formal writing * Use the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks) to explore voice options **Problem:** Voice-over pacing does not match your expectations. **Solution:** * Adjust the `speed` parameter: * Decrease to 80-90 for slower narration * Increase to 110-120 for faster delivery * Test different speeds to find the sweet spot * Consider your audience - educational content needs slower pacing * Very complex content may need speeds as low as 75 **Problem:** Narration timing seems off compared to visuals. **Solution:** * The API automatically syncs voice to video duration * Adjust scene breaks (`createSceneOnNewLine`, `createSceneOnEndOfSentence`) * Keep scenes to reasonable lengths (5-15 seconds each) * If using manual scenes, ensure text length matches desired scene duration * Review the completed video - timing may feel different than expected **Problem:** Voice-over sounds muffled, distorted, or has audio artifacts. **Solution:** * Reduce `amplificationLevel` to 0 or below * Avoid levels above 0.7 which can cause clipping * Try a different AI voice - some have better audio quality * Check for unusual characters or formatting in source text * Ensure text does not have excessive special characters **Problem:** None of the voices seem right for your content. **Solution:** * Use the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks) to see all options * Test multiple voices with the same content * Consider the voice characteristics table above for guidance * Try adjusting speed and amplification with different voices * Some voices work better for specific content types ## Next Steps Enhance your voice-over videos with these features: Use different voices or settings for different scenes Add music to complement voice-over narration Add translated or custom subtitles to your videos Create videos without voice-over ## API Reference For complete technical details, see: List all available AI voices Direct video rendering with voice-over Create preview before rendering Monitor storyboard creation progress # Multi-Level Voice-Over Source: https://docs.pictory.ai/guides/text-to-video/multi-level-voiceover Create videos with both video-level and scene-level voice-over settings for customized narration This guide shows you how to create videos with sophisticated voice-over configurations. Learn to set a default voice for your entire video while customizing specific scenes with different voice settings, speeds, or amplification levels. ## What You'll Learn Set a default voice for all scenes Customize voice for specific scenes Control speed and amplification per voice Use different AI voices in one video ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Basic understanding of voice-over concepts ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Multi-Level Voice-Over Works Multi-level voice-over gives you granular control over narration: 1. **Video-Level Settings**: Define default voice-over configuration for all scenes 2. **Scene-Level Overrides**: Customize voice settings for specific scenes 3. **Automatic Fallback**: Scenes without custom settings use the video-level default 4. **Flexible Control**: Mix and match voices, speeds, and volumes throughout your video Scene-level voice-over settings always override video-level settings for that specific scene. This allows precise control while maintaining consistency across your video. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Different text for different scenes const INTRO_TEXT = "Welcome to our comprehensive guide on AI in education."; const MAIN_TEXT = "AI is transforming how educators create content, automate workflows, and personalize learning experiences."; const OUTRO_TEXT = "Thank you for watching. Subscribe for more insights."; async function createVideoWithMultiLevelVoiceOver() { try { console.log("Creating video with multi-level voice-over..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "multilevel_voiceover_demo", // Video-level voice-over (default for all scenes) voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 100, // Normal speed amplificationLevel: 0, // Normal volume }, ], }, scenes: [ { story: INTRO_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // Scene-specific voice-over (slower and louder for emphasis) voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 85, // 15% slower for emphasis amplificationLevel: 0.3, // Slightly louder }, ], }, }, { story: MAIN_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // No scene-level voice-over = uses video-level default }, { story: OUTRO_TEXT, createSceneOnNewLine: false, createSceneOnEndOfSentence: false, // Different scene-level settings for outro voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 90, // Slightly slower amplificationLevel: 0.1, // Slightly louder }, ], }, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoWithMultiLevelVoiceOver(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Different text for different scenes INTRO_TEXT = "Welcome to our comprehensive guide on AI in education." MAIN_TEXT = "AI is transforming how educators create content, automate workflows, and personalize learning experiences." OUTRO_TEXT = "Thank you for watching. Subscribe for more insights." def create_video_with_multilevel_voiceover(): try: print("Creating video with multi-level voice-over...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'multilevel_voiceover_demo', # Video-level voice-over (default for all scenes) 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 100, # Normal speed 'amplificationLevel': 0 # Normal volume } ] }, 'scenes': [ { 'story': INTRO_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, # Scene-specific voice-over (slower and louder for emphasis) 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 85, # 15% slower for emphasis 'amplificationLevel': 0.3 # Slightly louder } ] } }, { 'story': MAIN_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False # No scene-level voice-over = uses video-level default }, { 'story': OUTRO_TEXT, 'createSceneOnNewLine': False, 'createSceneOnEndOfSentence': False, # Different scene-level settings for outro 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 90, # Slightly slower 'amplificationLevel': 0.1 # Slightly louder } ] } } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_video_with_multilevel_voiceover() ``` ## Understanding the Configuration ### Video-Level Voice-Over (Default Settings) | Parameter | Type | Default | Description | | ----------------------------------------- | ------- | ------- | -------------------------------------- | | `voiceOver.enabled` | boolean | false | Enable voice-over for the entire video | | `voiceOver.aiVoices` | array | - | Array of AI voice configurations | | `voiceOver.aiVoices[].speaker` | string | - | AI voice name (e.g., "Brian", "Emma") | | `voiceOver.aiVoices[].speed` | number | 100 | Voice speed (50-200) | | `voiceOver.aiVoices[].amplificationLevel` | number | 0 | Volume level (-1 to 1) | ### Scene-Level Voice-Over (Overrides) | Parameter | Type | Description | | ----------------------------- | ------- | ------------------------------------------------------------------ | | `scenes[].voiceOver` | object | Scene-specific voice-over settings (same structure as video-level) | | `scenes[].voiceOver.enabled` | boolean | Enable/disable voice-over for this scene | | `scenes[].voiceOver.aiVoices` | array | Custom voice configuration for this scene | **Override Behavior:** When you define scene-level voice-over, it completely replaces the video-level settings for that scene. If you want to use the same voice but different settings, you must specify the voice name again. ## Voice Speed Reference | Speed Value | Playback Rate | Best Used For | | ----------- | --------------------------- | --------------------------------------------- | | 50 | 0.5x (Very slow) | Complex technical content, learning materials | | 75 | 0.75x (Slower) | Detailed explanations, emphasis | | 85-90 | 0.85-0.9x (Slightly slower) | Introduction, important points | | 100 | 1.0x (Normal) | Standard content, most scenes | | 110-125 | 1.1-1.25x (Slightly faster) | Casual content, transitions | | 150 | 1.5x (Fast) | Quick summaries, energetic content | | 200 | 2.0x (Very fast) | Speed reading, urgent calls-to-action | ## Amplification Level Reference | Level | Effect | Best Used For | | ----- | --------------------- | -------------------------------- | | -1.0 | Quietest (background) | Subtle narration, ambient voice | | -0.5 | Quieter than normal | De-emphasized content | | 0 | Normal volume | Standard narration | | 0.3 | Slightly louder | Mild emphasis | | 0.5 | Moderately louder | Important points | | 1.0 | Loudest | Strong emphasis, calls-to-action | **Volume Considerations:** Very high amplification levels (0.7-1.0) may cause audio distortion. Test your settings and use moderation for professional results. ## Common Use Cases ### Emphasizing Key Sections ```javascript theme={null} // Intro: Slower and louder for impact voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 85, amplificationLevel: 0.3 }] } // Main content: Normal settings // (uses video-level default) // Call-to-action: Faster and louder voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", speed: 110, amplificationLevel: 0.5 }] } ``` ### Tutorial Videos ```javascript theme={null} // Step explanations: Slower for clarity voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 90, amplificationLevel: 0 }] } // Quick transitions: Faster pace voiceOver: { enabled: true, aiVoices: [{ speaker: "Emma", speed: 120, amplificationLevel: 0 }] } ``` ### Multi-Language Support ```javascript theme={null} // English sections voiceOver: { enabled: true, aiVoices: [{ speaker: "Brian", // English voice speed: 100, amplificationLevel: 0 }] } // Spanish sections voiceOver: { enabled: true, aiVoices: [{ speaker: "Maria", // Spanish voice speed: 100, amplificationLevel: 0 }] } ``` ## Best Practices Don't vary speed too dramatically between scenes - sudden changes can be jarring: * **Good:** Vary by 10-20 points (e.g., 90-110) * **Avoid:** Extreme jumps (e.g., 50 to 200) * **Tip:** Test your video to ensure smooth transitions Subtle volume changes are more professional than dramatic ones: * **Good:** Use 0.1-0.3 for emphasis * **Moderate:** Use 0.5 for strong emphasis * **Avoid:** Levels above 0.7 unless intentional * **Tip:** Let the content quality drive emphasis, not just volume Keep voice settings consistent across similar scenes: * Use the same voice for all sections * Apply similar speed/volume to similar content types * Create a "voice style guide" for your brand Always review a test video before creating multiple videos: * Generate a short sample with your settings * Listen on different devices (phone, desktop, headphones) * Adjust based on feedback * Document successful settings for reuse ## Troubleshooting **Problem:** Scene is not using your custom voice-over settings. **Solution:** * Ensure scene-level `voiceOver` object is properly formatted * Check that you have included the `speaker` name (it must be specified even if using the same voice) * Verify JSON syntax is correct (commas, brackets, quotes) **Problem:** Audio quality is poor or distorted. **Solution:** * Reduce `amplificationLevel` (try 0.3 or lower) * Avoid combining high speed (>150) with high amplification (>0.5) * Check that speed values are within 50-200 range **Problem:** Scene-level overrides are not being applied. **Solution:** * Verify scene-level `voiceOver` is inside the scene object * Check that `enabled: true` is set at scene level * Ensure scene-level configuration is complete (speaker, speed, amplificationLevel) ## Next Steps Enhance your voice-over videos with these features: Add music to complement voice-over narration Add translated or custom subtitles to your videos Apply consistent branding across all your videos Learn the basics of AI voice-over narration ## API Reference For complete technical details, see: List all available AI voices Direct video rendering with multi-level voice-over Create preview before rendering Monitor storyboard creation progress # Basic Text to Video Source: https://docs.pictory.ai/guides/text-to-video/text-to-video-basic Create a simple video from text using the Pictory API with automatic visual selection This guide shows you how to turn written text into an engaging video with automatically selected visuals. Perfect for creating social media content, educational videos, or marketing materials from your written content. ## What You'll Learn Transform written content into engaging videos AI selects visuals based on your text content Control how text is split into scenes Track video creation progress in real-time ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * The required packages installed ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Step-by-Step Guide ### Step 1: Set Up Your Request First, prepare your API credentials and the text you want to convert: ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API key // Your text content - this will be converted into video const SAMPLE_TEXT = "AI is transforming how we create content. By automating video production, " + "creators can focus on strategy while AI handles the technical work. " + "This means faster turnaround times and consistent quality across all videos."; ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Replace with your actual API key # Your text content - this will be converted into video SAMPLE_TEXT = ( "AI is transforming how we create content. By automating video production, " "creators can focus on strategy while AI handles the technical work. " "This means faster turnaround times and consistent quality across all videos." ) ``` ### Step 2: Create the Video Send your text to the API to start the video creation process: ```javascript Node.js theme={null} async function createTextToVideo() { try { console.log("Creating video from text..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "my_first_text_video", scenes: [ { story: SAMPLE_TEXT, createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); return jobId; } catch (error) { console.error("Error creating video:", error.response?.data || error.message); throw error; } } ``` ```python Python theme={null} def create_text_to_video(): try: print("Creating video from text...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'my_first_text_video', 'scenes': [ { 'story': SAMPLE_TEXT, 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") return job_id except requests.exceptions.RequestException as error: print(f"Error creating video: {error}") raise ``` ### Step 3: Monitor Progress Check the status of your video until it is ready: ```javascript Node.js theme={null} async function waitForVideo(jobId) { console.log("\nMonitoring video creation..."); while (true) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { console.log("\n✓ Video is ready!"); console.log("Video URL:", statusResponse.data.data.videoURL); return statusResponse.data; } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } // Wait 5 seconds before checking again await new Promise(resolve => setTimeout(resolve, 5000)); } } // Run the complete workflow createTextToVideo() .then(jobId => waitForVideo(jobId)) .then(result => console.log("\nDone!")) .catch(error => console.error("Error:", error)); ``` ```python Python theme={null} def wait_for_video(job_id): print("\nMonitoring video creation...") while True: response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) response.raise_for_status() status = response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': print("\n✓ Video is ready!") print(f"Video URL: {response.json()['data']['videoURL']}") return response.json() elif status == 'failed': raise Exception(f"Video creation failed: {response.json()}") # Wait 5 seconds before checking again time.sleep(5) # Run the complete workflow if __name__ == '__main__': job_id = create_text_to_video() result = wait_for_video(job_id) print("\nDone!") ``` ## Understanding the Parameters ### Required Parameters | Parameter | Type | Description | | ---------------- | ------ | ------------------------------------------------------------ | | `videoName` | string | A unique name for your video project (used for organization) | | `scenes` | array | List of scenes to include in your video (minimum 1 scene) | | `scenes[].story` | string | The text content that will be converted to video | ### Scene Control Options | Parameter | Type | Default | Description | | ---------------------------- | ------- | ------- | --------------------------------------------------- | | `createSceneOnNewLine` | boolean | false | Creates a new scene at each line break in your text | | `createSceneOnEndOfSentence` | boolean | false | Creates a new scene at the end of each sentence | **Scene Creation Best Practices:** * For short, punchy videos (like social media): Set both `createSceneOnNewLine` and `createSceneOnEndOfSentence` to `true` * For longer, narrative videos: Set both to `false` to keep related content together * For paragraph-based content: Set `createSceneOnNewLine` to `true` only ## How Scene Creation Works The AI automatically determines scene breaks based on your settings: ### Both Options Enabled (Recommended for Social Media) ```javascript theme={null} { createSceneOnNewLine: true, createSceneOnEndOfSentence: true } ``` **Result:** Maximum scene breaks - new scene for each sentence AND paragraph **Best for:** Short-form content, social media clips, dynamic videos ### Only Sentence Breaks ```javascript theme={null} { createSceneOnNewLine: false, createSceneOnEndOfSentence: true } ``` **Result:** New scene at each sentence, paragraphs stay together **Best for:** Educational content, explainer videos ### Only Line Breaks ```javascript theme={null} { createSceneOnNewLine: true, createSceneOnEndOfSentence: false } ``` **Result:** New scene at each paragraph/line break **Best for:** Article-to-video, blog content ### No Automatic Breaks ```javascript theme={null} { createSceneOnNewLine: false, createSceneOnEndOfSentence: false } ``` **Result:** All text stays in one scene **Best for:** Short quotes, single-thought videos ## What Happens Behind the Scenes When you submit your text, Pictory's AI: 1. **Analyzes Your Text** - Understands the context and key concepts 2. **Breaks Into Scenes** - Splits text based on your scene settings 3. **Selects Visuals** - Automatically chooses relevant stock images/videos for each scene 4. **Generates Captions** - Creates animated text overlays synchronized with the content 5. **Renders Video** - Compiles everything into a polished video file **Processing Time:** Videos typically take 3-10 minutes to create, depending on length. The API processes asynchronously, so you receive a job ID immediately and can check status periodically. ## Understanding the Response ### Initial Response (Job Created) ```json theme={null} { "success": true, "data": { "jobId": "abc123def456", "status": "in-progress" } } ``` ### Completed Response ```json theme={null} { "success": true, "data": { "jobId": "abc123def456", "status": "completed", "videoURL": "https://cdn.pictory.ai/videos/your-video.mp4", "duration": 45.5, "scenes": 5 } } ``` ## Common Use Cases ### Marketing Content ```javascript theme={null} const marketingText = "Introducing our new product! " + "50% faster than competitors. " + "Available now for $99."; ``` ### Educational Videos ```javascript theme={null} const lessonText = "Today we'll learn about photosynthesis. " + "Plants convert sunlight into energy. " + "This process produces oxygen for us to breathe."; ``` ### Social Media Posts ```javascript theme={null} const socialText = "5 tips for better productivity:\n" + "1. Start with your hardest task\n" + "2. Take regular breaks\n" + "3. Minimize distractions"; ``` ## Troubleshooting Common Issues **Problem:** Your API key is incorrect or expired. **Solution:** 1. Check that your API key starts with `pictai_` 2. Verify your subscription is active at [API Access page](https://app.pictory.ai/api-access) 3. Make sure you are passing it in the `Authorization` header **Problem:** The job may have encountered an issue. **Solution:** 1. Check the job status one more time 2. Verify your text content is not excessively long (keep under 5,000 characters for best results) 3. [Contact support](https://pictory.ai/contact) with your job ID if it has been over an hour **Problem:** The `story` field is empty or missing. **Solution:** Ensure each scene has a `story` field with actual text content: ```javascript theme={null} scenes: [{ story: "Your text here", // Must not be empty createSceneOnNewLine: true }] ``` **Problem:** Scene creation settings do not match your expectations. **Solution:** * Review the scene control options above * Test with both `createSceneOnNewLine` and `createSceneOnEndOfSentence` set to `true` for maximum control * Use manual line breaks (`\n`) in your text where you want scene breaks ## Next Steps Now that you know the basics, enhance your videos with these features: Add professional narration to your videos Translate or customize subtitle text Use AI to generate unique visuals instead of stock Add your logo, colors, and fonts automatically ## API Reference For complete technical details, see: Direct video rendering from text (used in this guide) Create preview before rendering Monitor storyboard creation progress Customize subtitle appearance # Text to Video with Captions Source: https://docs.pictory.ai/guides/text-to-video/text-to-video-captions Create videos with custom captions in different languages, separate from the story text This guide shows you how to create videos with custom captions that are separate from your story text. Perfect for creating multilingual content where the visuals are selected based on one language while displaying subtitles in another. ## What You'll Learn Add subtitles separate from story text Display captions in different languages Story for visuals, captions for display Perfect for international audiences ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Your content text and translated captions prepared * Language codes for your target language ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Caption System Works Pictory's caption system uses a dual-text approach: 1. **Story Text** - The main content used by AI to understand context and select appropriate visuals 2. **Caption Text** - The text displayed as subtitles in your final video 3. **Language Code** - Specifies the caption language for proper formatting 4. **Automatic Sync** - Captions are automatically synchronized with video timing This separation allows you to create videos in one language (for visual selection) while displaying subtitles in another language, making your content accessible to global audiences. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Story text - used for AI visual selection and content understanding const STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media."; // Caption text - displayed as subtitles (Spanish translation) const CAPTION_TEXT = "La IA está destinada a impactar significativamente a los educadores y creadores de cursos en las redes sociales."; async function createTextToVideoWithCaption() { try { console.log("Creating video with custom captions..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "text_to_video_with_caption", scenes: [ { story: STORY_TEXT, // English for visual selection caption: CAPTION_TEXT, // Spanish for subtitles captionLanguage: "es", // Spanish language code createSceneOnNewLine: false, // Required when using captions createSceneOnEndOfSentence: false, // Required when using captions }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video creation started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Video with captions is ready!"); console.log("Video URL:", jobResult.data.videoURL); } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createTextToVideoWithCaption(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Story text - used for AI visual selection and content understanding STORY_TEXT = "AI is poised to significantly impact educators and course creators on social media." # Caption text - displayed as subtitles (Spanish translation) CAPTION_TEXT = "La IA está destinada a impactar significativamente a los educadores y creadores de cursos en las redes sociales." def create_text_to_video_with_caption(): try: print("Creating video with custom captions...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'text_to_video_with_caption', 'scenes': [ { 'story': STORY_TEXT, # English for visual selection 'caption': CAPTION_TEXT, # Spanish for subtitles 'captionLanguage': 'es', # Spanish language code 'createSceneOnNewLine': False, # Required when using captions 'createSceneOnEndOfSentence': False # Required when using captions } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video creation started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Video with captions is ready!") print(f"Video URL: {job_result['data']['videoURL']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") raise if __name__ == '__main__': create_text_to_video_with_caption() ``` ## Caption Configuration Parameters ### Scene Configuration | Parameter | Type | Required | Description | | ---------------------------- | ------- | -------- | -------------------------------------------------------------------- | | `story` | string | Yes | The main text used for AI visual selection and content understanding | | `caption` | string | Yes | The text that will be displayed as subtitles in the video | | `captionLanguage` | string | Yes | ISO 639-1 language code for the caption (e.g., "es" for Spanish) | | `createSceneOnNewLine` | boolean | Yes | Must be `false` when using captions | | `createSceneOnEndOfSentence` | boolean | Yes | Must be `false` when using captions | **Important Limitation:** When using custom captions, both `createSceneOnNewLine` and `createSceneOnEndOfSentence` must be set to `false`. This means you need to manually create multiple scenes if you want scene breaks with captions. ## Supported Caption Languages | Language | Code | Language | Code | | -------- | ---- | ---------- | ---- | | Chinese | `zh` | Portuguese | `pt` | | Dutch | `nl` | Russian | `ru` | | English | `en` | Spanish | `es` | | French | `fr` | Hindi | `hi` | | German | `de` | Japanese | `ja` | | Italian | `it` | Korean | `ko` | ## Story vs Caption: Understanding the Difference **Story text** is the content the AI uses to: * Understand the context and meaning of your video * Select appropriate stock visuals and images * Determine the overall theme and mood * Generate relevant background content Use story text in the language that best describes your visual needs, even if your audience speaks a different language. **Caption text** is what your viewers will see: * Displayed as subtitles throughout the video * Can be a translation of the story text * Can be simplified or adapted for your audience * Should match your target audience's language Use caption text in the language your viewers will understand. **Scenario:** Creating a video for Spanish-speaking students about AI in education. ```javascript theme={null} { story: "AI is transforming education through personalized learning", caption: "La IA está transformando la educación mediante el aprendizaje personalizado", captionLanguage: "es" } ``` The AI understands the English concept to select educational visuals, but Spanish subtitles appear for your audience. ## Common Use Cases ### Creating Multilingual Marketing Videos ```javascript theme={null} { story: "Our product saves you time and increases productivity", caption: "Notre produit vous fait gagner du temps et augmente la productivité", captionLanguage: "fr" } ``` **Result:** Professional marketing video with French subtitles, using English-sourced visuals. ### Educational Content for International Students ```javascript theme={null} { story: "Learn programming fundamentals step by step", caption: "プログラミングの基礎を段階的に学ぶ", captionLanguage: "ja" } ``` **Result:** Programming tutorial with Japanese subtitles, maintaining clear technical visuals. ### Accessibility and Localization ```javascript theme={null} { story: "Welcome to our company presentation", caption: "Willkommen zu unserer Unternehmenspräsentation", captionLanguage: "de" } ``` **Result:** Corporate presentation accessible to German-speaking audiences. ## Working with Multiple Scenes Since automatic scene breaks are disabled when using captions, you need to manually create scenes: ```javascript theme={null} scenes: [ { story: "First concept in English", caption: "Primer concepto en español", captionLanguage: "es", createSceneOnNewLine: false, createSceneOnEndOfSentence: false }, { story: "Second concept in English", caption: "Segundo concepto en español", captionLanguage: "es", createSceneOnNewLine: false, createSceneOnEndOfSentence: false } ] ``` ## Best Practices Ensure your caption text accurately reflects the story text: * Use professional translation services when possible * Keep the meaning and tone consistent * Avoid direct word-for-word translations that may sound unnatural * Test with native speakers before production Always use the correct ISO 639-1 language code: * Double-check the code matches your caption language * Use region-specific codes when necessary (e.g., `zh-CN` vs `zh-TW`) * Verify the language is supported (see table above) Keep captions readable within the scene duration: * Shorter captions are easier to read * Break long sentences into multiple scenes * Aim for 1-2 lines of text per scene * Test video playback to ensure captions are comfortable to read Since automatic scene breaks do not work with captions: * Plan scene breaks manually before creating the video * Group related content into logical scenes * Create separate scene objects for each caption segment * Consider the visual flow between scenes ## Troubleshooting **Problem:** You set `createSceneOnNewLine` or `createSceneOnEndOfSentence` to `true` with captions. **Solution:** Set both parameters to `false`: ```javascript theme={null} { caption: "Your caption text", captionLanguage: "es", createSceneOnNewLine: false, // Must be false createSceneOnEndOfSentence: false // Must be false } ``` **Problem:** The caption text displays but formatting is incorrect. **Solution:** * Verify you are using the correct language code (e.g., "es" not "spanish") * Check the supported languages table above * Ensure the caption text is properly encoded (UTF-8) **Problem:** The selected visuals do not seem relevant to your captions. **Solution:** Remember that visuals are selected based on `story` text, not `caption` text: * Make sure your `story` text clearly describes the visuals you want * Keep `story` text in a language the AI handles well (English recommended) * The `caption` is only for subtitle display, not visual selection **Problem:** Captions are cut off or hard to read. **Solution:** * Break long text into multiple scenes * Keep each caption to 1-2 lines (approximately 60-100 characters) * Create separate scene objects for longer content: ```javascript theme={null} scenes: [ { story: "Part 1", caption: "Parte 1 del contenido", captionLanguage: "es" }, { story: "Part 2", caption: "Parte 2 del contenido", captionLanguage: "es" } ] ``` ## Next Steps Enhance your captioned videos with these features: Add narration in the caption language Customize caption appearance and formatting Use different voices for different scenes Apply consistent branding across videos ## API Reference For complete technical details, see: Direct video rendering with captions Customize subtitle appearance Create preview before rendering Monitor storyboard creation progress # Update Storyboard Elements Source: https://docs.pictory.ai/guides/video-storyboard/update-storyboard-elements Learn how to modify background visuals, text, and music in a storyboard preview before rendering the final video This guide walks you through updating storyboard elements after creating a preview. You'll learn how to replace background visuals, modify scene text, change background music, and render the final video with your updates. ## What You'll Learn Swap scene background images or videos with your own media Modify scene text and title content Replace background music with a different track Render the final video with all your modifications ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * A completed storyboard preview job (see [Create Storyboard Preview](/api-reference/videos/create-storyboard-preview)) ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## Workflow Overview The update workflow fits between creating a storyboard preview and rendering the final video: ```mermaid theme={null} flowchart LR A[Create Storyboard Preview] --> B[Poll Job Status] B --> C[Review Elements] C --> D[Update Elements] D --> E[Render 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: ```javascript Node.js theme={null} 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; } ``` ```python Python theme={null} 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 the `renderParams.elements` array to identify elements you want to update: ```javascript Node.js theme={null} 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); ``` ```python Python theme={null} 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) ``` **Example output:** ``` 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 automating tasks like ... 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 ```javascript Node.js theme={null} 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" ); ``` ```python Python theme={null} 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 `` 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. ```javascript Node.js theme={null} 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 automating repetitive tasks, AI frees up time for creative work and strategic thinking." ); ``` ```python Python theme={null} 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 automating repetitive tasks, AI frees up time for creative work and strategic thinking." ) ``` #### Change Background Music ```javascript Node.js theme={null} 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"); ``` ```python Python theme={null} 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 ```javascript Node.js theme={null} 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: "AI automation 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); ``` ```python Python theme={null} 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": "AI automation 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](/api-reference/videos/render-from-preview): ```javascript Node.js theme={null} 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); ``` ```python Python theme={null} 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 the `renderParams.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 `` 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](/api-reference/video-storyboard/update-storyboard-elements#element-attributes-reference) in the API documentation. ## Next Steps Preview updates in real-time using the embedded player Find stock videos and images to use as backgrounds Find background music tracks Full API reference for Update Storyboard Elements # Video to Short Clips Source: https://docs.pictory.ai/guides/video-to-shorts/video-highlights Repurpose long-form videos into short, engaging clips for social media This guide shows you how to transform long-form videos into short, engaging clips perfect for social media. Use AI to automatically extract the most impactful moments from webinars, interviews, lectures, or any long video content. ## What You'll Learn Convert long videos into short clips Automatically detect key moments Perfect for TikTok, Reels, Shorts Subtitles generated automatically ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Node.js or Python installed on your machine * Long-form video accessible via public URL * Understanding of target platform requirements ```bash npm theme={null} npm install axios ``` ```bash pip theme={null} pip install requests ``` ## How Video-to-Shorts Works When you convert a long video to short clips: 1. **Video Access** - Your source video is downloaded and analyzed 2. **Audio Transcription** - AI transcribes the audio content 3. **Content Analysis** - Speech patterns, visual changes, and content relevance are analyzed 4. **Highlight Detection** - Key moments and engaging segments are identified 5. **Clip Extraction** - Short clips are generated from the highlights 6. **Enhancement** - Clips are optimized with captions and formatting 7. **Video Rendering** - Final short clips are ready for download The source video must be accessible via a public URL. Processing time depends on video length - expect approximately 1-2 minutes of processing for every minute of source video. ## Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Sample video URL - replace with your own long-form video URL const VIDEO_URL = "https://pictory-static.pictorycontent.com/sample_long_video.mp4"; async function createVideoToShorts() { try { console.log("Creating short clips from long video..."); const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "video_to_shorts", scenes: [ { videoUrl: VIDEO_URL, // Long-form video URL at scene level audioLanguage: "en-US", // Required for video processing } ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); const jobId = response.data.data.jobId; console.log("✓ Video processing started!"); console.log("Job ID:", jobId); // Monitor progress console.log("\nMonitoring video creation..."); let jobCompleted = false; let jobResult = null; while (!jobCompleted) { const statusResponse = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY }, } ); const status = statusResponse.data.data.status; console.log("Status:", status); if (status === "completed") { jobCompleted = true; jobResult = statusResponse.data; console.log("\n✓ Short clips are ready!"); console.log("Video URL:", jobResult.data.videoURL); if (jobResult.data.highlights) { console.log("Highlight clips:", jobResult.data.highlights); } } else if (status === "failed") { throw new Error("Video creation failed: " + JSON.stringify(statusResponse.data)); } await new Promise(resolve => setTimeout(resolve, 5000)); } return jobResult; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideoToShorts(); ``` ```python Python theme={null} import requests import time import json API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Sample video URL - replace with your own long-form video URL VIDEO_URL = "https://pictory-static.pictorycontent.com/sample_long_video.mp4" def create_video_to_shorts(): try: print("Creating short clips from long video...") response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'video_to_shorts', 'scenes': [ { 'videoUrl': VIDEO_URL, # Long-form video URL at scene level 'audioLanguage': 'en-US' # Required for video processing } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) response.raise_for_status() job_id = response.json()['data']['jobId'] print("✓ Video processing started!") print(f"Job ID: {job_id}") # Monitor progress print("\nMonitoring video creation...") job_completed = False job_result = None while not job_completed: status_response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={'Authorization': API_KEY} ) status_response.raise_for_status() status = status_response.json()['data']['status'] print(f"Status: {status}") if status == 'completed': job_completed = True job_result = status_response.json() print("\n✓ Short clips are ready!") print(f"Video URL: {job_result['data']['videoURL']}") if 'highlights' in job_result['data']: print(f"Highlight clips: {job_result['data']['highlights']}") elif status == 'failed': raise Exception(f"Video creation failed: {status_response.json()}") time.sleep(5) return job_result except requests.exceptions.RequestException as error: print(f"Error: {error}") if hasattr(error, 'response') and error.response is not None: print(f"Response: {error.response.text}") raise if __name__ == '__main__': create_video_to_shorts() ``` ## Understanding the Parameters ### Main Request Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ----------------------------------------- | | `videoName` | string | Yes | A descriptive name for your video project | | `scenes` | array | Yes | Array of scene objects | ### Scene Parameters | Parameter | Type | Required | Description | | ------------------------ | ------ | -------- | ----------------------------------------------------- | | `videoUrl` | string | Yes | Public URL to the long-form source video file | | `audioLanguage` | string | Yes | Language code for audio transcription (e.g., "en-US") | | `mediaRepurposeSettings` | object | No | Optional settings for highlight extraction | ### Media Repurpose Settings (Optional) | Parameter | Type | Description | | ------------------------- | ------- | -------------------------------------------- | | `highlightLength` | number | Target highlight duration in seconds (5-180) | | `removeFillerWords` | boolean | Remove filler words from transcription | | `removeSilences` | boolean | Remove silent portions from video | | `silenceThresholdSeconds` | number | Silence threshold in seconds (0-10) | ## Supported Video Formats | Format | Extension | Description | Quality | | ------ | --------- | -------------------------------- | --------- | | MP4 | `.mp4` | Most common format (recommended) | Excellent | | MOV | `.mov` | Apple QuickTime format | Excellent | | AVI | `.avi` | Windows video format | Good | | MKV | `.mkv` | Matroska multimedia container | Excellent | | WebM | `.webm` | Web video format | Good | | WMV | `.wmv` | Windows Media Video | Good | **Best Format:** Use MP4 (H.264 codec) for the best balance of quality, compatibility, and processing speed. ## Ideal Clip Durations by Platform | Platform | Ideal Duration | Maximum Length | Notes | | --------------- | -------------- | -------------------- | --------------------------------------- | | TikTok | 15-60 seconds | 10 minutes | Shorter is better (under 60s) | | Instagram Reels | 15-90 seconds | 90 seconds | Sweet spot: 30-60 seconds | | YouTube Shorts | 15-60 seconds | 60 seconds | Must be vertical or square | | Twitter/X | 15-45 seconds | 2 minutes 20 seconds | Concise content performs best | | LinkedIn | 30-90 seconds | 10 minutes | Professional, value-focused | | Facebook | 30-90 seconds | 240 minutes | Attention span: first 3 seconds crucial | ## Common Use Cases ### Webinar Highlights for LinkedIn ```javascript theme={null} { videoName: "webinar_key_moments", scenes: [{ videoUrl: "https://storage.example.com/full-webinar.mp4", audioLanguage: "en-US" }] } ``` **Result:** Key insights extracted as 30-90 second clips perfect for LinkedIn. ### Podcast to Social Media Clips ```javascript theme={null} { videoName: "podcast_episode_highlights", scenes: [{ videoUrl: "https://storage.example.com/podcast-ep-042.mp4", audioLanguage: "en-US" }] } ``` **Result:** Best moments from podcast as shareable social clips. ### Interview Highlights for YouTube Shorts ```javascript theme={null} { videoName: "interview_best_moments", scenes: [{ videoUrl: "https://storage.example.com/full-interview.mp4", audioLanguage: "en-US", mediaRepurposeSettings: { highlightLength: 60, // 60-second highlights removeFillerWords: true } }] } ``` **Result:** Most engaging interview segments as short-form content. ### Educational Content for TikTok ```javascript theme={null} { videoName: "lecture_key_points", scenes: [{ videoUrl: "https://storage.example.com/full-lecture.mp4", audioLanguage: "en-US", mediaRepurposeSettings: { highlightLength: 30, // 30-second highlights for TikTok removeSilences: true } }] } ``` **Result:** Educational highlights perfect for TikTok's short format. ## Best Practices Start with high-quality source material: * **Resolution:** 1080p or higher recommended * **Audio Quality:** Clear speech with minimal background noise * **Lighting:** Well-lit content produces better clips * **Composition:** Centered subjects work best for crops * **Engagement:** Videos with natural segment breaks work better Select appropriate source video duration: * **10-30 Minutes:** Ideal length for extracting multiple clips * **Under 10 Minutes:** May produce fewer highlight options * **Over 60 Minutes:** Consider splitting into segments first * **Clear Sections:** Videos with distinct topics produce better clips * **Pacing:** Varied pacing helps AI identify engaging moments Audio quality directly affects clip selection: * **Clear Speech:** AI relies on transcription for context * **Minimal Noise:** Reduce background sounds * **Good Microphones:** Use quality recording equipment * **One Speaker:** Single speaker is easier to process * **Natural Pauses:** Helps AI identify segment boundaries Ensure your video file can be accessed: * **Cloud Storage:** Upload to Google Drive, Dropbox, AWS S3, or CDN * **Public Link:** Generate direct, public download URL * **No Authentication:** URL must work without login * **Test Access:** Open in incognito browser to verify * **Stable URL:** Ensure link will not expire during processing Consider your target platform: * **Aspect Ratio:** 9:16 (vertical) for TikTok, Reels, Shorts * **Duration:** Match platform limits (15-60s typical) * **Captions:** Auto-generated captions included * **Branding:** Add logos and brand elements * **Hook:** First 3 seconds crucial for retention ## Troubleshooting **Problem:** The API cannot download or process your video. **Solution:** * Verify URL is publicly accessible (test in incognito browser) * Ensure it is a direct download link, not streaming or preview link * For Google Drive: Share → "Anyone with the link" → Copy link * For Dropbox: Create link → change "dl=0" to "dl=1" in URL * Check file hasn't been deleted or moved * Verify video format is supported (MP4, MOV, AVI, MKV, WebM) **Problem:** Job status shows "in-progress" for extended periods. **Solution:** * Processing time is proportional to video length * Expected times: * 10-minute video: 15-20 minutes processing * 30-minute video: 30-45 minutes processing * 60-minute video: 60-90 minutes processing * Large file sizes take longer to download * Check status every 5-10 seconds (not more frequently) * If stuck for 2x expected time, contact support with job ID **Problem:** AI does not extract the segments you expected. **Solution:** * AI identifies engaging moments based on multiple factors * Ensure important moments have: * Clear, distinct speech * Visual changes or action * Natural pauses before/after * Energy in delivery * Consider manually selecting specific segments instead * Longer source videos give more clip options **Problem:** Generated clips have muffled or unclear audio. **Solution:** * Check source video audio quality * Use higher bitrate audio in source (128kbps minimum) * Reduce background noise in source video * Ensure consistent audio levels throughout * Re-export source video with better audio settings * Try audio enhancement tools before uploading **Problem:** Auto-generated captions do not match audio. **Solution:** * Improve source audio quality * Reduce background music volume * Ensure clear speech (not too fast) * Minimize overlapping speakers * Check for audio distortion or clipping * Consider professional audio editing **Problem:** API completes but does not return highlight clips. **Solution:** * Verify videoToVideo parameter is set to true * Check source video contains clear speech * Ensure video is not purely music or sound effects * Try with different source video to test * Verify source video is not corrupted * Contact support if issue persists with valid content ## Next Steps Enhance your short-form videos with these features: Add music to your short clips Customize auto-generated captions Apply consistent branding to clips Add your logo to all clips ## API Reference For complete technical details, see: * [Render Storyboard Video](/api-reference/videos/render-storyboard-video) - Full API specification * [Get Job Status](/api-reference/jobs/get-video-render-job-by-id) - Monitor job status and progress # Avatar Positioning Source: https://docs.pictory.ai/guides/video-with-avatar/avatar-positioning Master avatar placement and styling for professional-looking videos **🎉 New Feature!** Learn how to position and style AI avatars in your videos for maximum impact. ## Overview Strategic avatar positioning ensures your avatar complements rather than competes with your video content. This guide covers positioning techniques, styling options, and best practices for different video formats. *** ## Positioning Methods There are two ways to position your avatar: ### 1. Preset Positions (Recommended for Quick Setup) Use the `position` parameter with predefined locations: ```json theme={null} { "avatar": { "position": "bottom-right", "width": "20%" } } ``` **Available Positions:** Upper left corner Top center Upper right corner Middle left Center screen Middle right Lower left corner Bottom center Lower right corner *** ### 2. Custom Positioning (Scene-Level Only) Use `top` and `left` parameters for pixel-perfect placement. These are only available as **scene-level avatar overrides**, not at the global avatar level. ```json theme={null} { "scenes": [ { "story": "Scene with custom avatar placement.", "avatar": { "top": "18%", "left": "76%", "width": "20%" } } ] } ``` Distance from top edge as percentage (e.g., "10%", "25%", "50%"). Integer percentages only (0%-100%). Distance from left edge as percentage (e.g., "5%", "50%", "80%"). Integer percentages only (0%-100%). **Important:** `top`/`left` are only available at the scene level. Cannot use both `position` and `top`/`left` together in the same scene override. *** ## Avatar Sizing Control avatar size with the `width` parameter: ```json theme={null} { "avatar": { "position": "bottom-right", "width": "20%" } } ``` ### Recommended Sizes by Video Type | Video Type | Recommended Width | Use Case | | ---------- | ------------------ | --------------------------------------------- | | **15-18%** | Subtle presence | Background-focused videos, product demos | | **20-25%** | Balanced presence | Most use cases, tutorials, training | | **25-30%** | Prominent presence | Presenter-style, talking head, announcements | | **30%+** | Dominant presence | Interview-style, personal vlogs, testimonials | **Pro Tip:** Start with 20% width and adjust based on your background content density. Busier backgrounds work better with smaller avatars. *** ## Styling Options ### Border Radius Control the avatar container shape: ```json theme={null} { "avatar": { "borderRadius": "100" } } ``` | Value | Shape | Best For | | ------- | ---------------- | ------------------------------ | | `"0"` | Square corners | Modern, technical content | | `"10"` | Slightly rounded | Soft professional look | | `"50"` | Rounded corners | Friendly, approachable style | | `"100"` | Circular | Most popular, clean appearance | *** ### Border Styling Add visual separation with borders: ```json theme={null} { "avatar": { "borderColor": "rgba(185,185,186,1)", "borderThickness": 4 } } ``` Border color in RGBA format (e.g., "rgba(255,255,255,1)", "rgba(0,0,0,1)", "rgba(132,44,254,1)") Border width in pixels. Minimum: 0, no maximum limit. Recommended: 2-4 pixels *** ### Background Color Add a background behind your avatar for better visibility: ```json theme={null} { "avatar": { "backgroundColor": "rgba(255,255,255,1)" } } ``` **Color Format:** RGBA - `rgba(red, green, blue, alpha)` * Red, Green, Blue: 0-255 * Alpha (opacity): 0-1 **Common Background Colors:** ```json theme={null} // Pure white (opaque) "backgroundColor": "rgba(255,255,255,1)" // Light gray "backgroundColor": "rgba(244,241,246,1)" // Semi-transparent white "backgroundColor": "rgba(255,255,255,0.9)" // Pure black (opaque) "backgroundColor": "rgba(0,0,0,1)" // Semi-transparent black "backgroundColor": "rgba(0,0,0,0.8)" ``` **Contrast Tip:** Use light backgrounds for dark video backgrounds and vice versa to ensure avatar visibility. *** ## Position Examples ### Example 1: Bottom-Right Corner (Default) **Best for:** Most videos, non-intrusive placement ```json theme={null} { "avatar": { "position": "bottom-right", "width": "20%", "borderRadius": "100", "backgroundColor": "rgba(255,255,255,1)" } } ``` **Use when:** * Subtitles are at the bottom center * Background has important left-side content * You want minimal distraction *** ### Example 2: Center-Left (Presenter Style) **Best for:** Training videos, professional presentations ```json theme={null} { "avatar": { "position": "center-left", "width": "25%", "borderRadius": "50", "backgroundColor": "rgba(244,241,246,1)" } } ``` **Use when:** * Background content is on the right * Presenter-focused content * Formal training or education *** ### Example 3: Custom Top-Right Position (Scene-Level) **Best for:** Unique layouts, specific design requirements ```json theme={null} { "scenes": [ { "story": "Scene with custom avatar placement.", "avatar": { "top": "10%", "left": "75%", "width": "22%", "borderRadius": "100", "backgroundColor": "rgba(255,255,255,0.95)" } } ] } ``` **Use when:** * You need precise alignment per scene * Matching specific brand guidelines * Complex multi-element layouts Custom `top`/`left` positioning is only available at the scene level. For global positioning, use the preset `position` parameter. *** ### Example 4: Circular Avatar with Colored Border **Best for:** Branded content, modern style ```json theme={null} { "avatar": { "position": "bottom-left", "width": "18%", "borderRadius": "100", "borderColor": "rgba(132,44,254,1)", "borderThickness": 6, "backgroundColor": "rgba(255,255,255,1)" } } ``` **Use when:** * Incorporating brand colors * Need visual accent * Premium, polished appearance *** ## Position by Video Format ### 16:9 (Landscape / YouTube) **Recommended Positions:** * `bottom-right` or `bottom-left` (20-22% width) * `center-right` or `center-left` (22-25% width) **Avoid:** * `bottom-center` (conflicts with subtitles) ```json theme={null} { "videoWidth": 1920, "videoHeight": 1080, "avatar": { "position": "bottom-right", "width": "20%" } } ``` *** ### 9:16 (Vertical / TikTok, Instagram Reels) **Recommended Positions:** * `top-center` (25-30% width) * `bottom-center` (20-25% width, if no subtitles) * `center-center` (30%+ width for presenter style) **Avoid:** * Side positions (wasted space in vertical format) ```json theme={null} { "videoWidth": 1080, "videoHeight": 1920, "avatar": { "position": "top-center", "width": "28%" } } ``` *** ### 1:1 (Square / Instagram Feed) **Recommended Positions:** * `bottom-right` or `bottom-left` (20-25% width) * `top-right` or `top-left` (20-25% width) **Avoid:** * Center positions (takes too much space) ```json theme={null} { "videoWidth": 1080, "videoHeight": 1080, "avatar": { "position": "bottom-right", "width": "22%" } } ``` *** ## Avoiding Common Mistakes **Problem:** Avatar covers important subtitle text **Solution:** * Place avatar in corners, not bottom-center * Use `top` positioning to avoid subtitle area * Reduce avatar size if necessary * Consider scene-level positioning adjustments ```json theme={null} // Good: Avatar in corner, subtitles in center { "avatar": { "position": "bottom-right", "width": "20%" } } // Bad: Avatar in center where subtitles appear { "avatar": { "position": "bottom-center", "width": "30%" } } ``` **Problem:** Avatar blends into background **Solution:** * Add background color with good contrast * Use border for separation * Adjust avatar position to clearer area * Consider semi-transparent backgrounds ```json theme={null} // Good: White background on dark video { "avatar": { "backgroundColor": "rgba(255,255,255,0.95)", "borderColor": "rgba(204,204,204,1)", "borderThickness": 2 } } ``` **Problem:** Avatar jumps around between scenes **Solution:** * Use global avatar config for consistency * Only override position when truly needed * Keep width consistent across scenes * Plan position changes deliberately ```json theme={null} // Good: Consistent global position { "avatar": { "position": "bottom-right", "width": "20%" }, "scenes": [ { "story": "Scene 1..." }, { "story": "Scene 2..." } ] } ``` **Problem:** Avatar dominates the frame **Solution:** * Reduce width to 15-25% for most content * Only use 30%+ for presenter-focused videos * Consider background importance * Test different sizes ```json theme={null} // Balanced for most content { "width": "20%" } // Presenter-focused { "width": "30%" } ``` *** ## Advanced Positioning Techniques ### Technique 1: Responsive Positioning by Scene Adapt avatar position based on background content. Use scene-level `top`/`left` overrides to avoid covering important UI elements: ```javascript theme={null} const scenes = [ { story: 'Welcome to our product demo.', // Uses global avatar position (e.g., bottom-right) }, { story: 'Here is the product interface.', background: { visualUrl: 'https://example.com/interface.jpg', type: 'image' }, // Scene-level override: move avatar to avoid covering UI avatar: { top: '10%', left: '5%', width: '18%' } }, { story: 'Now let us look at the features.', // Returns to global avatar position } ]; ``` *** ### Technique 2: Brand-Matched Styling Align avatar styling with brand colors: ```javascript theme={null} const brandedAvatar = { avatar: { position: 'bottom-right', width: '20%', borderRadius: '50', borderColor: 'rgba(132,44,254,1)', // Brand purple borderThickness: 4, backgroundColor: 'rgba(132,44,254,0.1)' // Light purple tint } }; ``` *** ### Technique 3: Dynamic Positioning for Multi-Avatar Effect Create variety while maintaining consistency: ```javascript theme={null} const scenes = [ { story: 'Introduction to topic A.', avatar: { position: 'bottom-right', width: '20%' } }, { story: 'Now for topic B.', avatar: { position: 'bottom-left', width: '20%' } }, { story: 'Finally, topic C.', avatar: { position: 'bottom-right', width: '20%' } } ]; ``` *** ## Quick Reference ### Preset Position Cheat Sheet ```javascript theme={null} // Most popular positions const popularPositions = { general: { position: 'bottom-right', width: '20%' }, presenter: { position: 'center-left', width: '25%' }, vertical: { position: 'top-center', width: '28%' }, discreet: { position: 'bottom-left', width: '15%' } }; ``` *** ### Styling Template ```javascript theme={null} const stylingTemplate = { // Clean, professional professional: { borderRadius: '100', borderColor: 'rgba(185,185,186,1)', borderThickness: 2, backgroundColor: 'rgba(255,255,255,1)' }, // Modern, colorful modern: { borderRadius: '50', borderColor: 'rgba(132,44,254,1)', borderThickness: 4, backgroundColor: 'rgba(244,241,246,1)' }, // Minimal, transparent minimal: { borderRadius: '100', borderThickness: 0, backgroundColor: 'rgba(255,255,255,0.85)' } }; ``` *** ## Testing Your Positioning Generate a short test video with your chosen position View on actual devices (mobile, desktop, TV) where your audience will watch Test with various background types (dark, light, busy, simple) Ensure subtitles do not overlap avatar Fine-tune position, size, and styling based on results *** ## Next Steps Learn to customize avatars per scene Complete guide to creating avatar videos Full technical documentation Customize subtitle positioning and styling # Create Video with Avatar Source: https://docs.pictory.ai/guides/video-with-avatar/create-avatar-video Learn how to create engaging videos with AI avatars using the Pictory API **🎉 New Feature!** AI Avatars are now available in the Pictory API. Create presenter-style videos with lifelike avatars that lip-sync to AI narration - no cameras or studios required. ## Introduction AI avatars bring a human presence to your videos without requiring cameras, studios, or on-screen talent. This guide walks you through creating professional avatar-powered videos using the Pictory API. Avatar videos combine AI voice-over with lifelike avatar animations that lip-sync to your narration, creating engaging presenter-style content perfect for training, marketing, education, and social media. *** ## Quick Start Here's a minimal example to create your first avatar video: ```javascript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'my_first_avatar_video', language: 'en', videoWidth: 1920, videoHeight: 1080, // Voice-over is required for avatar videos voiceOver: { enabled: true, aiVoices: [{ speaker: 'Matthew', speed: 100 }] }, // Avatar configuration avatar: { avatarId: 'Annie_expressive12_public', position: 'bottom-right', width: '20%' }, // Your content scenes: [{ story: 'Welcome to my video! Today we will explore how AI avatars make video creation simple and engaging.', createSceneOnEndOfSentence: true, minimumDuration: 4 }] }) }); const result = await response.json(); console.log('Job ID:', result.data.jobId); ``` *** ## AI Credits Avatar video generation consumes AI credits from your account. Ensure you have sufficient AI credits before using the Avatar feature. Avatar generation costs **15 AI credits per 30 seconds** of video. Plan your video length accordingly to manage credit usage. *** ## Prerequisites Before creating avatar videos, you need: 1. **API Key** - Get your API key from the Pictory dashboard 2. **Avatar Details** - Use the [Get Avatars](/api-reference/avatars/get-avatars) API to retrieve available `avatarId` values 3. **Voice Selection** - Choose an AI voice from the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) API 4. **AI Credits** - Sufficient AI credits in your account (15 credits per 30 seconds of avatar video) *** ## Step-by-Step Guide ### Step 1: Configure Basic Video Settings Start with the essential video parameters: ```json theme={null} { "videoName": "corporate_training_video", "language": "en", "videoWidth": 1920, "videoHeight": 1080, "saveProject": true } ``` Give your video a descriptive name (alphanumeric, spaces, underscores, hyphens) Content language: `en`, `es`, `fr`, `de`, `it`, `pt`, `ja`, `ko`, `zh`, `nl`, `ru`, `hi` Video width in pixels (e.g., 1920 for Full HD) Video height in pixels (e.g., 1080 for Full HD) Save project for future editing (recommended: `true`) *** ### Step 2: Set Up Voice-Over Voice-over is **required** when using avatars - the avatar will lip-sync to the narration: ```json theme={null} { "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "Matthew", "speed": 100, "amplificationLevel": 0 } ] } } ``` **Voice Selection Tips:** * Match voice gender to your avatar when possible * Use professional voices (Brian, Matthew, Emma) for business content * Adjust speed to 90-110 for natural delivery * Get available voices from [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) *** ### Step 3: Configure Your Avatar Add the avatar configuration with appearance and positioning: ```json theme={null} { "avatar": { "avatarId": "Annie_expressive12_public", "position": "bottom-right", "width": "20%", "borderRadius": "100", "borderColor": "rgba(185,185,186,1)", "borderThickness": 4, "backgroundColor": "rgba(255,255,255,1)" } } ``` #### Required Avatar Fields Unique identifier for the avatar (e.g., "Annie\_expressive12\_public"). Get from [Get Avatars](/api-reference/avatars/get-avatars) API. #### Optional Styling Preset position: `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Default: `bottom-right` Avatar width as percentage. Must be an integer percentage from 0% to 100% (e.g., "20%", "25%", "30%"). Decimal percentages like "25.5%" are not supported. Default: `"20%"` Recommended: 15-30% for optimal balance Corner rounding: `0` (square), `50` (rounded), `100` (circular) Background color in RGBA format (e.g., "rgba(255,255,255,1)") *** ### Step 4: Add Your Content Define scenes with the text your avatar will narrate: ```json theme={null} { "scenes": [ { "story": "Welcome to Pictory. In this video, we will explore how AI avatars work.", "createSceneOnEndOfSentence": true, "minimumDuration": 4 }, { "story": "You can create professional videos without cameras or studios.", "createSceneOnEndOfSentence": true, "minimumDuration": 4, "background": { "type": "video", "visualUrl": "https://example.com/background.mp4", "settings": { "mute": true, "loop": true } } } ] } ``` **Scene Best Practices:** * Use `createSceneOnEndOfSentence: true` for natural pacing * Set minimum duration of 3-5 seconds * Keep individual scenes under 15 seconds * Use clear, conversational language *** ### Step 5: Add Background Music (Optional) Enhance your video with background music: ```json theme={null} { "backgroundMusic": { "enabled": true, "autoMusic": true, "volume": 0.15 } } ``` **Volume Recommendations:** * Keep music volume between 0.1-0.2 when using voice-over * Higher volumes may overpower the avatar's narration * Test different levels to find the right balance *** ## Complete Example Here's a full example with all features: ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'complete_avatar_demo', language: 'en', videoWidth: 1920, videoHeight: 1080, saveProject: true, voiceOver: { enabled: true, aiVoices: [{ speaker: 'Matthew', speed: 100, amplificationLevel: 0 }] }, backgroundMusic: { enabled: true, autoMusic: true, volume: 0.15 }, avatar: { avatarId: 'Annie_expressive12_public', position: 'bottom-right', width: '20%', borderRadius: '100', borderColor: 'rgba(185,185,186,1)', borderThickness: 4, backgroundColor: 'rgba(255,255,255,1)' }, scenes: [ { story: 'Welcome to our training session. Today we will cover three important topics.', createSceneOnEndOfSentence: true, minimumDuration: 4 }, { story: 'First, we will explore the fundamentals of our product.', createSceneOnEndOfSentence: true, minimumDuration: 4 }, { story: 'Then we will learn about best practices and common use cases.', createSceneOnEndOfSentence: true, minimumDuration: 4 } ] }) }); const result = await response.json(); console.log('Job ID:', result.data.jobId); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'complete_avatar_demo', 'language': 'en', 'videoWidth': 1920, 'videoHeight': 1080, 'saveProject': True, 'voiceOver': { 'enabled': True, 'aiVoices': [{ 'speaker': 'Matthew', 'speed': 100, 'amplificationLevel': 0 }] }, 'backgroundMusic': { 'enabled': True, 'autoMusic': True, 'volume': 0.15 }, 'avatar': { 'avatarId': 'Annie_expressive12_public', 'position': 'bottom-right', 'width': '20%', 'borderRadius': '100', 'borderColor': 'rgba(185,185,186,1)', 'borderThickness': 4, 'backgroundColor': 'rgba(255,255,255,1)' }, 'scenes': [ { 'story': 'Welcome to our training session. Today we will cover three important topics.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4 }, { 'story': 'First, we will explore the fundamentals of our product.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4 }, { 'story': 'Then we will learn about best practices and common use cases.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4 } ] } ) result = response.json() print(f"Job ID: {result['data']['jobId']}") ``` ```bash cURL theme={null} curl --request POST \ --url https://api.pictory.ai/pictoryapis/v2/video/storyboard \ --header 'Authorization: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "videoName": "complete_avatar_demo", "language": "en", "videoWidth": 1920, "videoHeight": 1080, "saveProject": true, "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Matthew", "speed": 100, "amplificationLevel": 0 }] }, "backgroundMusic": { "enabled": true, "autoMusic": true, "volume": 0.15 }, "avatar": { "avatarId": "Annie_expressive12_public", "position": "bottom-right", "width": "20%", "borderRadius": "100", "borderColor": "rgba(185,185,186,1)", "borderThickness": 4, "backgroundColor": "rgba(255,255,255,1)" }, "scenes": [ { "story": "Welcome to our training session. Today we will cover three important topics.", "createSceneOnEndOfSentence": true, "minimumDuration": 4 } ] }' ``` *** ## Monitoring Progress After creating your avatar video, poll the job status: ```javascript theme={null} // Poll for job completion const checkStatus = async (jobId) => { const response = await fetch( `https://api.pictory.ai/pictoryapis/v1/jobs/${jobId}`, { headers: { 'Authorization': 'YOUR_API_KEY' } } ); const job = await response.json(); if (job.data.status === 'completed') { console.log('Video ready!'); console.log('Preview URL:', job.data.previewUrl); console.log('Project URL:', job.data.projectUrl); console.log('Video URL:', job.data.videoURL); } else if (job.data.status === 'failed') { console.error('Video creation failed'); } else { console.log('Status:', job.data.status); // Poll again after 5 seconds setTimeout(() => checkStatus(jobId), 5000); } }; // Start polling checkStatus(result.data.jobId); ``` *** ## Common Use Cases Create training modules with a professional avatar instructor: * Use formal voices (Matthew, Brian) * Position avatar prominently (center-left or center-right) * Include clear topic transitions * Add relevant background visuals Showcase products with an avatar guide: * Use friendly voices (Emma, Joanna) * Position avatar in bottom corner to not block product * Keep narration concise and benefits-focused * Use product images/videos as backgrounds Create engaging social posts: * Use 9:16 aspect ratio (vertical) * Position avatar prominently (center or top-third) * Keep videos under 60 seconds * Use dynamic voice pacing Build course content with avatar teachers: * Use clear, measured voice pacing * Position avatar consistently across lessons * Break content into 5-10 minute segments * Include visual aids in backgrounds *** ## Best Practices * Write in conversational tone * Use short, clear sentences * Include natural pauses * Test pronunciation of technical terms * Don't overlap subtitles or key visuals * Keep size between 15-25% of frame * Use consistent positioning * Consider background contrast * Set music volume to 0.1-0.2 * Use clear AI voices * Adjust speed to 90-110 * Test audio clarity * 4-5 seconds minimum per scene * Natural sentence breaks * Add pauses between topics * Keep scenes under 15 seconds *** ## Troubleshooting **Issue:** Avatar does not show in video **Solutions:** * Verify all required avatar fields are provided * Check avatar URLs are accessible * Ensure voiceOver is enabled with at least one AI voice * Validate avatarId field **Issue:** Avatar lip movements do not match audio **Solutions:** * Ensure voice-over is properly configured * Check that speaker is available for your plan * Try adjusting voice speed to 100 * Contact support if issue persists **Issue:** Voice or music volume is off **Solutions:** * Adjust `amplificationLevel` (-1 to 1) * Reduce background music volume (0.1-0.2) * Test different volume combinations * Keep music instrumental only **Issue:** Avatar overlaps important content **Solutions:** * Try different preset positions * Use custom top/left positioning * Reduce avatar width * Adjust per-scene if needed *** ## Next Steps Browse available avatars and their looks Learn advanced positioning techniques Customize avatars per scene Full API documentation Master AI voice-over settings # Scene-Level Customization Source: https://docs.pictory.ai/guides/video-with-avatar/scene-level-customization Dynamically adjust avatar appearance and behavior for each scene **🎉 New Feature!** Customize avatar appearance per scene for dynamic, professional videos. ## Overview Scene-level avatar customization allows you to adapt your avatar's appearance, position, and visibility for each individual scene. This creates dynamic, professional videos that respond to changing content and backgrounds. Scene-level settings override global avatar configuration. Any property not specified at the scene level inherits from the global avatar settings. *** ## How It Works ### Global vs Scene-Level Configuration ```javascript theme={null} { // Global avatar configuration (applies to all scenes) "avatar": { "avatarId": "...", "position": "bottom-right", "width": "20%", "backgroundColor": "rgba(255,255,255,1)" }, "scenes": [ { // Scene 1: Uses global avatar settings "story": "Introduction scene" }, { // Scene 2: Overrides position and background color "story": "Demo scene with custom layout", "avatar": { "top": "10%", "left": "5%", "backgroundColor": "rgba(244,241,246,1)" } }, { // Scene 3: Hides avatar completely "story": "Focus on content without avatar", "avatar": { "hide": true } } ] } ``` **Pro Tip:** Define your default avatar configuration globally, then only override specific properties in scenes where you need different behavior. *** ## Common Use Cases ### Use Case 1: Hide Avatar for Specific Scenes Hide the avatar to focus attention on content: ```javascript theme={null} { "avatar": { "avatarId": "Annie_expressive12_public", "position": "bottom-right", "width": "20%" }, "scenes": [ { "story": "Welcome to our product demo. I will guide you through the features.", // Avatar visible (uses global config) }, { "story": "Here is our product interface.", "background": { "visualUrl": "https://example.com/product-screenshot.jpg", "type": "image" }, // Hide avatar to show full interface "avatar": { "hide": true } }, { "story": "Now let me explain how it works.", // Avatar returns (uses global config again) } ] } ``` **When to hide avatar:** * Showing detailed interfaces or dashboards * Displaying full-screen graphics * Focusing on text-heavy content * Transition scenes or title cards *** ### Use Case 2: Reposition for Background Content Move avatar to avoid covering important content: ```javascript theme={null} { "avatar": { "avatarId": "...", "position": "bottom-right", // Default position "width": "20%" }, "scenes": [ { "story": "Let me show you our dashboard.", "background": { "visualUrl": "https://example.com/dashboard-right-panel.jpg", "type": "image" }, // Move to left since important content is on right "avatar": { "position": "bottom-left" } }, { "story": "Here are the analytics charts.", "background": { "visualUrl": "https://example.com/charts-left-side.jpg", "type": "image" }, // Move to right since charts are on left "avatar": { "position": "bottom-right" } } ] } ``` *** ### Use Case 3: Adjust Size for Emphasis Vary avatar size based on scene importance: ```javascript theme={null} { "avatar": { "avatarId": "...", "position": "bottom-right", "width": "20%" // Standard size }, "scenes": [ { "story": "Welcome! I am excited to share this with you.", // Larger avatar for personal introduction "avatar": { "width": "28%", "position": "center-right" } }, { "story": "Here are the technical details and specifications.", // Smaller avatar to focus on content "avatar": { "width": "15%" } }, { "story": "Thank you for watching! Remember to subscribe.", // Larger avatar for closing statement "avatar": { "width": "25%" } } ] } ``` *** ### Use Case 4: Change Background Color by Scene Adapt avatar background for different video backgrounds: ```javascript theme={null} { "avatar": { "avatarId": "...", "position": "bottom-right", "width": "20%", "backgroundColor": "rgba(255,255,255,1)" // White by default }, "scenes": [ { "story": "This is our light-themed interface.", "background": { "color": "rgba(245,245,245,1)" // Light gray background } // Uses default white avatar background }, { "story": "And here is our dark mode.", "background": { "color": "rgba(30,30,30,1)" // Dark background }, // Dark avatar background for contrast "avatar": { "backgroundColor": "rgba(50,50,50,0.9)" } } ] } ``` *** ### Use Case 5: Custom Positioning for Complex Layouts Use precise positioning for specific scene requirements: ```javascript theme={null} { "avatar": { "avatarId": "...", "position": "bottom-right", "width": "20%" }, "scenes": [ { "story": "Standard introduction scene." // Uses preset position }, { "story": "Special layout scene.", "background": { "visualUrl": "https://example.com/complex-layout.jpg", "type": "image" }, // Fine-tuned positioning "avatar": { "top": "18%", "left": "76%", "width": "22%" } } ] } ``` *** ## Scene-Level Properties All avatar properties can be overridden at scene level: Override preset position: `top-left`, `top-center`, `top-right`, `center-left`, `center-center`, `center-right`, `bottom-left`, `bottom-center`, `bottom-right` Custom top position as percentage (e.g., "10%", "25%") Custom left position as percentage (e.g., "5%", "80%") Avatar width as percentage (e.g., "15%", "20%", "30%") Corner rounding: `"0"`, `"50"`, `"100"` Border color in RGBA format (e.g., "rgba(255,255,255,1)", "rgba(132,44,254,1)") Border width in pixels (0, 2, 4, 6, 8) Background color in RGBA (e.g., "rgba(255,255,255,1)") Hide avatar for this scene (`true` or `false`) **Important:** You cannot change `avatarId` at the scene level. The same avatar must be used throughout the video. You can only override styling and positioning properties. *** ## Advanced Patterns ### Pattern 1: Progressive Avatar Presence Start subtle, become more prominent, then fade out: ```javascript theme={null} { "scenes": [ { "story": "Opening title sequence.", "avatar": { "hide": true } }, { "story": "Hello, welcome to the presentation.", "avatar": { "width": "18%", "position": "bottom-right" } }, { "story": "Let me explain the key concepts in detail.", "avatar": { "width": "22%", "position": "center-right" } }, { "story": "Here are the detailed specifications.", "avatar": { "hide": true } }, { "story": "Thank you for watching!", "avatar": { "width": "25%", "position": "center-center" } }, { "story": "Closing credits.", "avatar": { "hide": true } } ] } ``` *** ### Pattern 2: Split-Screen Effect Position avatar on alternating sides for visual variety: ```javascript theme={null} { "scenes": [ { "story": "First point about product benefits.", "avatar": { "position": "center-left", "width": "25%" } }, { "story": "Second point about ease of use.", "avatar": { "position": "center-right", "width": "25%" } }, { "story": "Third point about pricing.", "avatar": { "position": "center-left", "width": "25%" } }, { "story": "Fourth point about support.", "avatar": { "position": "center-right", "width": "25%" } } ] } ``` *** ### Pattern 3: Background-Adaptive Positioning Automatically adjust based on background content: ```javascript theme={null} const createSceneWithAdaptiveAvatar = (story, backgroundUrl, contentPosition) => { const avatarPosition = { left: 'bottom-right', right: 'bottom-left', center: 'top-center', top: 'bottom-center', bottom: 'top-center' }; return { story, background: { visualUrl: backgroundUrl, type: 'image', settings: { mute: true, loop: true } }, avatar: { position: avatarPosition[contentPosition] } }; }; const scenes = [ createSceneWithAdaptiveAvatar( 'Content on left side', 'https://example.com/left-content.jpg', 'left' ), createSceneWithAdaptiveAvatar( 'Content on right side', 'https://example.com/right-content.jpg', 'right' ) ]; ``` *** ### Pattern 4: Themed Scenes with Matching Avatar Style Coordinate avatar styling with scene themes: ```javascript theme={null} { "scenes": [ { "story": "Welcome to our professional services overview.", // Professional theme: subtle, neutral "background": { "color": "rgba(240,240,245,1)" }, "avatar": { "backgroundColor": "rgba(255,255,255,1)", "borderColor": "rgba(204,204,204,1)", "borderThickness": 2, "borderRadius": "50" } }, { "story": "Now for our creative solutions!", // Creative theme: colorful, bold "background": { "color": "rgba(255,235,245,1)" }, "avatar": { "backgroundColor": "rgba(132,44,254,0.15)", "borderColor": "rgba(132,44,254,1)", "borderThickness": 4, "borderRadius": "100" } } ] } ``` *** ## Complete Example Here's a full video demonstrating multiple customization techniques: ```javascript JavaScript theme={null} const response = await fetch('https://api.pictory.ai/pictoryapis/v2/video/storyboard', { method: 'POST', headers: { 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ videoName: 'dynamic_avatar_demo', language: 'en', videoWidth: 1920, videoHeight: 1080, saveProject: true, voiceOver: { enabled: true, aiVoices: [{ speaker: 'Matthew', speed: 100 }] }, backgroundMusic: { enabled: true, autoMusic: true, volume: 0.15 }, // Global avatar configuration avatar: { avatarId: 'Annie_expressive12_public', position: 'bottom-right', width: '20%', borderRadius: '100', borderColor: 'rgba(185,185,186,1)', borderThickness: 4, backgroundColor: 'rgba(255,255,255,1)' }, scenes: [ { story: 'Welcome to our product demonstration. I will guide you through all the features.', createSceneOnEndOfSentence: true, minimumDuration: 4 // Uses global avatar settings }, { story: 'First, let me show you the main dashboard interface.', createSceneOnEndOfSentence: true, minimumDuration: 4, background: { type: 'image', visualUrl: 'https://example.com/dashboard.jpg', settings: { mute: true } }, // Hide avatar to show full interface avatar: { hide: true } }, { story: 'The dashboard provides real-time analytics and insights.', createSceneOnEndOfSentence: true, minimumDuration: 4 // Avatar returns with global settings }, { story: 'Now let us look at the settings panel on the right side.', createSceneOnEndOfSentence: true, minimumDuration: 4, background: { type: 'image', visualUrl: 'https://example.com/settings-right.jpg', settings: { mute: true } }, // Move avatar to left to avoid covering settings avatar: { position: 'bottom-left', width: '18%' } }, { story: 'Thank you for watching! Visit our website to learn more.', createSceneOnEndOfSentence: true, minimumDuration: 4, // Larger, centered avatar for closing avatar: { position: 'center-center', width: '28%' } } ] }) }); const result = await response.json(); console.log('Job ID:', result.data.jobId); ``` ```python Python theme={null} import requests response = requests.post( 'https://api.pictory.ai/pictoryapis/v2/video/storyboard', headers={ 'Authorization': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, json={ 'videoName': 'dynamic_avatar_demo', 'language': 'en', 'videoWidth': 1920, 'videoHeight': 1080, 'saveProject': True, 'voiceOver': { 'enabled': True, 'aiVoices': [{ 'speaker': 'Matthew', 'speed': 100 }] }, 'backgroundMusic': { 'enabled': True, 'autoMusic': True, 'volume': 0.15 }, # Global avatar configuration 'avatar': { 'avatarId': 'Annie_expressive12_public', 'position': 'bottom-right', 'width': '20%', 'borderRadius': '100', 'borderColor': 'rgba(185,185,186,1)', 'borderThickness': 4, 'backgroundColor': 'rgba(255,255,255,1)' }, 'scenes': [ { 'story': 'Welcome to our product demonstration. I will guide you through all the features.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4 # Uses global avatar settings }, { 'story': 'First, let me show you the main dashboard interface.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4, 'background': { 'type': 'image', 'visualUrl': 'https://example.com/dashboard.jpg', 'settings': {'mute': True} }, # Hide avatar to show full interface 'avatar': { 'hide': True } }, { 'story': 'Now let us look at the settings panel on the right side.', 'createSceneOnEndOfSentence': True, 'minimumDuration': 4, 'background': { 'type': 'image', 'visualUrl': 'https://example.com/settings-right.jpg', 'settings': {'mute': True} }, # Move avatar to left 'avatar': { 'position': 'bottom-left', 'width': '18%' } } ] } ) result = response.json() print(f"Job ID: {result['data']['jobId']}") ``` *** ## Best Practices **Before coding:** * Sketch your video flow * Identify scenes where avatar should hide * Mark scenes needing position changes * Note background content locations **Benefits:** * Smoother visual transitions * Better content presentation * Fewer adjustments needed **Guidelines:** * Keep avatar size consistent (±5% variation max) * Use the same styling throughout * Make position changes purposeful, not random * Return to default position when possible **Exception:** * Intro/outro scenes can have different sizes **Check for:** * Jarring position jumps * Sudden size changes * Style inconsistencies * Background color clashes **Fix with:** * Gradual position shifts * Smoother size transitions * Consistent styling * Scene-appropriate colors **Good reasons to hide:** * Full-screen demonstrations * Complex visual content * Title cards or transitions * Text-heavy information **Avoid hiding:** * For more than 2-3 consecutive scenes * Without clear visual benefit * Randomly throughout video *** ## Troubleshooting **Problem:** Scene-level settings not applying **Check:** * Property names match exactly (case-sensitive) * RGBA format is correct for colors * Percentage values include "%" symbol * Not trying to override avatarId (identity field cannot be changed at scene level) **Solution:** ```javascript theme={null} // Correct avatar: { "backgroundColor": "rgba(255,255,255,1)", "width": "20%" } // Incorrect avatar: { "background-color": "rgb(255,255,255)", // Wrong format "width": 20 // Missing % } ``` **Problem:** Avatar position changes too abruptly **Solutions:** 1. Use adjacent positions (e.g., bottom-right → bottom-center → bottom-left) 2. Add transition scene between major position changes 3. Keep position consistent for related content 4. Test preview to verify smoothness **Problem:** Avatar looks different across scenes **Check:** * Border settings are consistent * Background colors have proper contrast * Size variations are intentional * All scenes inherit global settings properly **Fix:** * Set global defaults for consistency * Only override when necessary * Document your overrides *** ## Quick Reference ### Scene Avatar Override Template ```javascript theme={null} // Copy this template for scene-level customization { "story": "Your scene text here", "createSceneOnEndOfSentence": true, "minimumDuration": 4, "avatar": { // Position (choose one method) "position": "bottom-right", // OR "top": "10%", "left": "80%", // Custom position // Size "width": "20%", // Styling "borderRadius": "100", "borderColor": "rgba(185,185,186,1)", "borderThickness": 4, "backgroundColor": "rgba(255,255,255,1)", // Visibility "hide": false } } ``` *** ## Next Steps Master positioning techniques and best practices Complete guide to creating avatar videos Full technical API documentation Configure background media settings # Using ElevenLabs Voices Source: https://docs.pictory.ai/guides/voice-over/elevenlabs-voices Use any ElevenLabs voice in your Pictory videos — pass the voice ID and it is automatically discovered and added to your library This guide shows you how to use any voice from the ElevenLabs catalog in your Pictory videos. Simply pass the ElevenLabs voice ID as the `speaker` value — Pictory will automatically discover and add the voice to your library. ## How It Works Browse the [ElevenLabs Voice Library](https://elevenlabs.io/voice-library) and copy the voice ID of any voice you want to use. Use the ElevenLabs voice ID directly in your API request as the `speaker` value. If the voice is not already in your Pictory library, it will be automatically discovered and added from ElevenLabs. No manual setup required. ## Finding an ElevenLabs Voice ID You can find voice IDs from: 1. **ElevenLabs Voice Library** — Browse voices at [elevenlabs.io/voice-library](https://elevenlabs.io/voice-library). The voice ID is shown on each voice's detail page. 2. **ElevenLabs API** — Use the [ElevenLabs Voices API](https://api.elevenlabs.io/v1/voices) to list available voices programmatically. 3. **Pictory Voiceover Tracks API** — Use the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) endpoint to list voices already in your library, including ElevenLabs voices with their `voice` field containing the ElevenLabs voice ID. ## Basic Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithElevenLabsVoice() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "elevenlabs_voice_demo", voiceOver: { enabled: true, aiVoices: [ { speaker: "pNInz6obpgDQGcFmaJgB", // ElevenLabs voice ID for "Adam" speed: 100, amplificationLevel: 0, }, ], }, scenes: [ { story: "Welcome to our product demo. Today we'll show you how easy it is to create professional videos with ultra-realistic AI narration.", createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, }, } ); console.log("Job ID:", response.data.data.jobId); } createVideoWithElevenLabsVoice(); ``` ```python Python theme={null} import requests API_BASE_URL = "https://api.pictory.ai/pictoryapis" API_KEY = "YOUR_API_KEY" def create_video_with_elevenlabs_voice(): response = requests.post( f"{API_BASE_URL}/v2/video/storyboard/render", json={ "videoName": "elevenlabs_voice_demo", "voiceOver": { "enabled": True, "aiVoices": [ { "speaker": "pNInz6obpgDQGcFmaJgB", # ElevenLabs voice ID for "Adam" "speed": 100, "amplificationLevel": 0, } ], }, "scenes": [ { "story": "Welcome to our product demo. Today we'll show you how easy it is to create professional videos with ultra-realistic AI narration.", "createSceneOnEndOfSentence": True, } ], }, headers={ "Content-Type": "application/json", "Authorization": API_KEY, }, ) response.raise_for_status() print(f"Job ID: {response.json()['data']['jobId']}") create_video_with_elevenlabs_voice() ``` ## Ways to Reference a Voice The `speaker` field accepts multiple formats: | Format | Example | Description | | ------------------- | ------------------------ | --------------------------------------------------- | | Voice name | `"Brian"` | Matches by voice name in your library | | Track ID | `"1776425060651"` | Numeric track ID from your library | | ElevenLabs voice ID | `"pNInz6obpgDQGcFmaJgB"` | ElevenLabs UUID — auto-discovered if not in library | ### Resolution Order When you pass a `speaker` value, Pictory resolves it in this order: 1. **Track ID** — If numeric, searches by track ID 2. **Voice name** — Searches by voice name (case-insensitive) 3. **Voice ID** — Searches by the voice provider's ID (e.g., ElevenLabs UUID) 4. **Auto-discovery** — If not found, attempts to discover and add the voice from ElevenLabs ## Using Previously Discovered Voices Once a voice is discovered and added to your library, you can reference it by name or track ID in future requests — just like any other voice. Use the [Get Voiceover Tracks](/api-reference/voiceovers/get-voiceover-tracks) endpoint to see all voices in your library: ```bash theme={null} curl --request GET \ --url https://api.pictory.ai/pictoryapis/v1/voiceovers/tracks \ --header 'Authorization: YOUR_API_KEY' ``` ElevenLabs voices in the response will have `"service": "elevenlabs"` and the original voice ID in the `voice` field. ## Premium Voice Settings For ElevenLabs voices, you can fine-tune the voice output using `premiumVoiceSettings`: ```javascript theme={null} { voiceOver: { enabled: true, aiVoices: [{ speaker: "pNInz6obpgDQGcFmaJgB", speed: 100, amplificationLevel: 0, premiumVoiceSettings: { modelId: "eleven_multilingual_v2", // ElevenLabs model stability: 50, // 0-100, higher = more consistent similarityBoost: 75, // 0-100, higher = closer to original style: 0, // 0-100, higher = more expressive useSpeakerBoost: true // Enhance voice clarity } }] } } ``` ### Premium Voice Settings Reference | Parameter | Type | Range | Description | | ----------------- | ------- | ----- | ----------------------------------------------------------------- | | `modelId` | string | - | ElevenLabs model ID (e.g., `eleven_multilingual_v2`, `eleven_v3`) | | `stability` | number | 0-100 | Voice consistency. Lower values produce more variation. | | `similarityBoost` | number | 0-100 | How closely the output matches the original voice. | | `style` | number | 0-100 | Expressiveness. Higher values add more emotion. | | `useSpeakerBoost` | boolean | - | Enhances voice clarity and presence. | `premiumVoiceSettings` only work with ElevenLabs voices. Using them with AWS or Google voices will return a validation error. ## Troubleshooting **Cause:** The ElevenLabs voice ID is invalid or the voice does not exist. **Resolution:** 1. Verify the ElevenLabs voice ID is correct 2. Check that the voice exists in the [ElevenLabs Voice Library](https://elevenlabs.io/voice-library) 3. Ensure your Pictory subscription includes ElevenLabs voice access **Cause:** Default voice settings may not match the ElevenLabs preview. **Resolution:** 1. Adjust `premiumVoiceSettings` — increase `similarityBoost` for closer matching 2. Try a different `modelId` — `eleven_multilingual_v2` and `eleven_v3` produce different results 3. Adjust `stability` — lower values produce more natural variation ## Next Steps List all voices in your library Complete guide to AI voiceover in videos Use different voices for different scenes Control how words are pronounced # Pronunciation Guide with SSML Phoneme Tags Source: https://docs.pictory.ai/guides/voice-over/pronunciation-guide Control how AI voices pronounce specific words using SSML phoneme tags in Pictory This guide explains how to customize word pronunciation in your video voice-overs using SSML (Speech Synthesis Markup Language) phoneme tags. Phoneme tags give you precise control over how AI voices pronounce brand names, technical terms, foreign words, or any text that might be mispronounced. ## What You'll Learn Understand SSML phoneme tag syntax and usage Learn how ElevenLabs, AWS Polly, and Google handle pronunciation Use the CMU Arpabet phonetic alphabet for pronunciation Apply pronunciation control to real video content ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * Basic understanding of the [Storyboard API](/api-reference/videos/render-storyboard-video) * Familiarity with [voice-over configuration](/guides/text-to-video/ai-voiceover) ## Understanding Voice-Over Types Pictory supports three voice-over services, each with different SSML phoneme support: | Service | Category | Phoneme Support | Alphabet | | -------------- | -------- | --------------- | ------------ | | **ElevenLabs** | Premium | English only | CMU Arpabet | | **AWS Polly** | Standard | Full support | IPA, X-SAMPA | | **Google TTS** | Standard | Full support | IPA | You can identify the voice-over service by the `service` field in the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks) response. Values are `elevenlabs`, `aws`, or `google`. *** ## Enabling SSML in Your Story To use SSML tags including phoneme tags, you must set `isSSMLStory: true` in your scene configuration: ```json theme={null} { "scenes": [ { "story": "Welcome to Pictory.", "isSSMLStory": true, "createSceneOnEndOfSentence": true } ] } ``` The `isSSMLStory` property is required when using any SSML tags. Without it, SSML tags will be read as plain text instead of being processed. *** ## ElevenLabs (Premium Voices) ElevenLabs premium voices provide high-quality, natural-sounding speech with phoneme support for English language content. ### Key Requirements 1. **Model Configuration**: When using phoneme tags with ElevenLabs, you must specify a `modelId` in `premiumVoiceSettings`. If not provided, `eleven_flash_v2` is used by default for phoneme processing. 2. **Limited Model Support**: Only three ElevenLabs models support phoneme tags. Other models will ignore phoneme markup. 3. **English Only**: Phoneme pronunciation control works only with English language content in ElevenLabs. 4. **CMU Arpabet**: ElevenLabs uses the CMU Arpabet phonetic alphabet. ### Models with Phoneme Support Only the following three models support SSML phoneme tags in ElevenLabs. Using phoneme tags with other models will not produce the expected pronunciation changes. | Model ID | Description | Phoneme Support | | ----------------------- | ---------------------------------------------------------------------- | --------------- | | `eleven_flash_v2` | Ultra-fast model (\~75ms latency), English only (default for phonemes) | Yes | | `eleven_turbo_v2` | High quality with low latency (\~250-300ms), English only | Yes | | `eleven_monolingual_v1` | First generation model, English only (deprecated) | Yes | ### Models Without Phoneme Support The following models do not support phoneme tags: | Model ID | Description | Phoneme Support | | ------------------------ | ---------------------------------------------------------------------- | --------------- | | `eleven_v3` | Latest and most advanced model, 70+ languages | No | | `eleven_multilingual_v2` | Most lifelike model with rich emotional expression, 29 languages | No | | `eleven_flash_v2_5` | Ultra-fast model (\~75ms latency), 32 languages, 50% lower cost | No | | `eleven_turbo_v2_5` | Balanced quality and speed (\~250-300ms), 32 languages, 50% lower cost | No | | `eleven_multilingual_v1` | First generation multilingual model, 8 languages (deprecated) | No | ### Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithPronunciation() { const storyWithPhonemes = ` Welcome to Pictory, the leading AI video creation platform. Our intuitive interface transforms your text into professional videos using advanced algorithms. Whether you are an entrepreneur or content creator, Pictory makes video production effortless. Watch our tutorial to learn how AI visualization can revolutionize your workflow. `; const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "pictory_pronunciation_demo", // Voice-over with ElevenLabs premium voice voiceOver: { enabled: true, aiVoices: [ { speaker: "Brian", speed: 100, premiumVoiceSettings: { modelId: "eleven_flash_v2" // Required for phoneme support } } ] }, backgroundMusic: { enabled: true, volume: 0.1, autoMusic: true }, scenes: [ { story: storyWithPhonemes, isSSMLStory: true, createSceneOnEndOfSentence: true } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); console.log("Job ID:", response.data.data.jobId); return response.data; } createVideoWithPronunciation(); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_pronunciation(): story_with_phonemes = ''' Welcome to Pictory, the leading AI video creation platform. Our intuitive interface transforms your text into professional videos using advanced algorithms. Whether you are an entrepreneur or content creator, Pictory makes video production effortless. Watch our tutorial to learn how AI visualization can revolutionize your workflow. ''' response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'pictory_pronunciation_demo', # Voice-over with ElevenLabs premium voice 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Brian', 'speed': 100, 'premiumVoiceSettings': { 'modelId': 'eleven_flash_v2' # Required for phoneme support } } ] }, 'backgroundMusic': { 'enabled': True, 'volume': 0.1, 'autoMusic': True }, 'scenes': [ { 'story': story_with_phonemes, 'isSSMLStory': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) print('Job ID:', response.json()['data']['jobId']) return response.json() create_video_with_pronunciation() ``` ### ElevenLabs External Documentation For detailed information about ElevenLabs pronunciation features: * [ElevenLabs Pronunciation Best Practices](https://elevenlabs.io/docs/overview/capabilities/text-to-speech/best-practices#pronunciation) *** ## AWS Polly (Standard Voices) AWS Polly voices provide reliable SSML support with multiple phonetic alphabets for precise pronunciation control. ### Key Features 1. **Multiple Alphabets**: AWS Polly supports IPA (International Phonetic Alphabet) and X-SAMPA phonetic systems. 2. **SSML Categories**: AWS Polly voices have different SSML support levels (Category A or B). Check the `ssmlSupportCategory` field from the tracks API. 3. **Neural and Standard Engines**: Different voices use different engines with varying SSML capabilities. ### Phoneme Tag Syntax ```xml theme={null} Pictory ``` Or using X-SAMPA: ```xml theme={null} Pictory ``` ### Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithAWSPolly() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "aws_polly_pronunciation_demo", // Voice-over with AWS Polly voice voiceOver: { enabled: true, aiVoices: [ { speaker: "Joanna", // AWS Polly neural voice speed: 100 } ] }, backgroundMusic: { enabled: true, volume: 0.1, autoMusic: true }, scenes: [ { story: `Welcome to Pictory. Transform your text into engaging videos with AI.`, isSSMLStory: true, createSceneOnEndOfSentence: true } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); console.log("Job ID:", response.data.data.jobId); return response.data; } createVideoWithAWSPolly(); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_aws_polly(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'aws_polly_pronunciation_demo', # Voice-over with AWS Polly voice 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Joanna', # AWS Polly neural voice 'speed': 100 } ] }, 'backgroundMusic': { 'enabled': True, 'volume': 0.1, 'autoMusic': True }, 'scenes': [ { 'story': '''Welcome to Pictory. Transform your text into engaging videos with AI.''', 'isSSMLStory': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) print('Job ID:', response.json()['data']['jobId']) return response.json() create_video_with_aws_polly() ``` ### AWS Polly External Documentation For detailed information about AWS Polly phoneme tags: * [AWS Polly Phoneme Tag Documentation](https://docs.aws.amazon.com/polly/latest/dg/phoneme-tag.html) *** ## Google Text-to-Speech (Standard Voices) Google TTS voices offer high-quality neural speech synthesis with comprehensive IPA phoneme support. ### Key Features 1. **IPA Support**: Google TTS uses the International Phonetic Alphabet (IPA) for phoneme specification. 2. **WaveNet and Neural2 Voices**: Google offers advanced neural voice engines with natural-sounding output. 3. **Multi-language**: Phoneme support across multiple languages with language-specific IPA symbols. ### Phoneme Tag Syntax ```xml theme={null} Pictory ``` ### Complete Example ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; async function createVideoWithGoogleTTS() { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "google_tts_pronunciation_demo", // Voice-over with Google TTS voice voiceOver: { enabled: true, aiVoices: [ { speaker: "Steffi", // Google WaveNet voice speed: 100 } ] }, backgroundMusic: { enabled: true, volume: 0.1, autoMusic: true }, scenes: [ { story: `Welcome to Pictory. Create professional videos using artificial intelligence.`, isSSMLStory: true, createSceneOnEndOfSentence: true } ] }, { headers: { "Content-Type": "application/json", Authorization: API_KEY } } ); console.log("Job ID:", response.data.data.jobId); return response.data; } createVideoWithGoogleTTS(); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' def create_video_with_google_tts(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'google_tts_pronunciation_demo', # Voice-over with Google TTS voice 'voiceOver': { 'enabled': True, 'aiVoices': [ { 'speaker': 'Steffi', # Google WaveNet voice 'speed': 100 } ] }, 'backgroundMusic': { 'enabled': True, 'volume': 0.1, 'autoMusic': True }, 'scenes': [ { 'story': '''Welcome to Pictory. Create professional videos using artificial intelligence.''', 'isSSMLStory': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY } ) print('Job ID:', response.json()['data']['jobId']) return response.json() create_video_with_google_tts() ``` ### Google TTS External Documentation For detailed information about Google TTS phoneme support: * [Google Cloud Text-to-Speech Phonemes](https://docs.cloud.google.com/text-to-speech/docs/phonemes) *** ## CMU Arpabet Reference The CMU Arpabet is a phonetic alphabet commonly used with ElevenLabs. Here's a quick reference: ### Vowels | Arpabet | Example | Word | | ------- | ------- | ---------- | | AA | ɑ | f**a**ther | | AE | æ | c**a**t | | AH | ʌ | c**u**t | | AO | ɔ | c**au**ght | | EH | ɛ | b**e**d | | ER | ɝ | b**ir**d | | IH | ɪ | b**i**t | | IY | i | b**ea**t | | UH | ʊ | b**oo**k | | UW | u | b**oo**t | ### Stress Markers | Marker | Meaning | | ------ | ---------------- | | 0 | No stress | | 1 | Primary stress | | 2 | Secondary stress | ### Example Breakdown For "Pictory" pronounced as `P IH1 K T AO0 R IY0`: | Symbol | Sound | | ------ | ---------------------------------- | | P | /p/ as in **p**at | | IH1 | /ɪ/ as in b**i**t (primary stress) | | K | /k/ as in **k**it | | T | /t/ as in **t**op | | AO0 | /ɔ/ as in c**au**ght (no stress) | | R | /r/ as in **r**un | | IY0 | /i/ as in b**ea**t (no stress) | *** ## Common Pronunciation Examples Here are phoneme representations for words commonly mispronounced: | Word | CMU Arpabet | IPA | | -------- | ------------------------- | ------------- | | Pictory | `P IH1 K T AO0 R IY0` | `ˈpɪktɔːri` | | AI | `EY1 AY1` | `ˌeɪˈaɪ` | | Video | `V IH1 D IY0 OW0` | `ˈvɪdioʊ` | | Tutorial | `T UW0 T AO1 R IY0 AH0 L` | `tuːˈtɔːriəl` | *** ## Best Practices Always test your phoneme tags with a short video before creating longer content. Different voices may interpret phonemes slightly differently. Stick to one phonetic alphabet per voice provider: * **ElevenLabs**: CMU Arpabet * **AWS Polly**: IPA or X-SAMPA * **Google TTS**: IPA Only use phoneme tags for words that are genuinely mispronounced. Overusing them can make content harder to maintain. Keep a reference document of phoneme tags used for your brand names and technical terms for consistency across videos. *** ## Troubleshooting **Problem:** The phoneme tags appear as literal text in the voice-over. **Solution:** Ensure `isSSMLStory: true` is set in your scene configuration. This flag enables SSML processing. **Problem:** The word is still mispronounced even with phoneme tags. **Solution:** * Verify you are using CMU Arpabet (not IPA) with ElevenLabs * Check that `premiumVoiceSettings.modelId` is specified * Ensure stress markers (0, 1, 2) are correctly placed **Problem:** Some voices do not process SSML tags correctly. **Solution:** Check the `ssmlSupportCategory` field from the [Get Voiceover Tracks API](/api-reference/voiceovers/get-voiceover-tracks). Some voices have limited SSML support. **Problem:** Request fails when using special characters in phoneme strings. **Solution:** Ensure proper escaping of special characters. In JSON, use `\"` for quotes within the phoneme attribute. *** ## Next Steps Learn the basics of adding voice-over to videos Use different voices for different scenes Discover all available AI voices Complete API reference for video rendering *** ## External Resources ElevenLabs pronunciation guide AWS Polly phoneme reference Google TTS phoneme guide # Introduction Source: https://docs.pictory.ai/index Get started with the Pictory API to create videos programmatically Welcome to the Pictory API. This guide will help you get started with automated video creation. Whether you are building an application, automating your content workflow, or integrating video capabilities into your platform, the Pictory API makes it straightforward. ## What You Can Do Turn written content into engaging videos with visuals Transform blog articles into video content automatically Convert presentations into shareable videos Extract short clips from long videos Add natural-sounding narration to your videos Generate and customize subtitles in multiple languages Maintain consistent brand styling across all your videos Auto-upload to AWS S3, Vimeo, and more ## What You Need Sign up for free at [app.pictory.ai](https://app.pictory.ai) if you do not have one Choose a plan that fits your usage from the [API Subscription](https://app.pictory.ai/api-access) page Your unique authentication credential for API access — details below ## Step 1: Create Your Account & Subscribe Follow these steps to set up your account: 1. Get started by creating a free account at [Pictory](https://app.pictory.ai) 2. Click on your profile picture in the top-right corner 3. Select [API Subscription](https://app.pictory.ai/api-access) from the menu 4. Choose a plan based on how many videos you plan to create per month 5. Complete the purchase to activate your API access How to buy API subscription ## Step 2: Get Your API Key Once you have an active subscription, you can find your API Key: 1. Go to the [API Subscription](https://app.pictory.ai/api-access) page in your Pictory account 2. Your API Key will be displayed on this page 3. Click the **Copy** button to copy it to your clipboard 4. Save it securely — you need it for every API request API Subscription Page showing API Key **Keep your API Key secure!** Treat it like a password. Never share it publicly or include it in client-side code that users can see. Anyone with your API Key can create videos using your account. ## Step 3: Make Your First API Call The following example demonstrates creating a video from text content: 1. **Include your API Key** in the `Authorization` request header for authentication 2. **Specify a video name** and provide the text content 3. **Receive a job ID** — the API returns this immediately for tracking the asynchronous rendering process Video rendering is asynchronous. The API returns a job ID that you can use to poll for completion status. ```javascript Node.js theme={null} import axios from "axios"; const API_BASE_URL = "https://api.pictory.ai/pictoryapis"; const API_KEY = "YOUR_API_KEY"; // Replace with your actual API Key async function createVideo() { try { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard/render`, { videoName: "my_first_video", scenes: [ { story: "Welcome to Pictory! This is my first video created with the API.", createSceneOnNewLine: true, createSceneOnEndOfSentence: true, }, ], }, { headers: { "Content-Type": "application/json", Authorization: API_KEY, // Use API Key directly }, } ); console.log("Job created:", response.data.data.jobId); return response.data; } catch (error) { console.error("Error:", error.response?.data || error.message); throw error; } } createVideo(); ``` ```python Python theme={null} import requests API_BASE_URL = 'https://api.pictory.ai/pictoryapis' API_KEY = 'YOUR_API_KEY' # Replace with your actual API Key def create_video(): response = requests.post( f'{API_BASE_URL}/v2/video/storyboard/render', json={ 'videoName': 'my_first_video', 'scenes': [ { 'story': 'Welcome to Pictory! This is my first video created with the API.', 'createSceneOnNewLine': True, 'createSceneOnEndOfSentence': True } ] }, headers={ 'Content-Type': 'application/json', 'Authorization': API_KEY # Use API Key directly } ) response.raise_for_status() print(f"Job created: {response.json()['data']['jobId']}") return response.json() if __name__ == '__main__': create_video() ``` ## Step 4: Check When Your Video is Ready Since video rendering is asynchronous, poll the job status endpoint to determine when your video is ready: 1. **Use the job ID from Step 3** to query the current status 2. **Poll the status endpoint every 10–30 seconds** until rendering completes 3. **When status is `completed`** — the response will include your video download URL ```javascript Node.js theme={null} async function checkJobStatus(jobId) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY, }, } ); const status = response.data.data.status; console.log("Status:", status); if (status === "completed") { console.log("Video URL:", response.data.data.videoURL); } return response.data; } ``` ```python Python theme={null} def check_job_status(job_id): response = requests.get( f'{API_BASE_URL}/v1/jobs/{job_id}', headers={ 'Authorization': API_KEY } ) response.raise_for_status() status = response.json()['data']['status'] print(f'Status: {status}') if status == 'completed': print(f"Video URL: {response.json()['data']['videoURL']}") return response.json() ``` **Understanding Status Values:** | Status | What It Means | What To Do | | ------------- | ------------------------------ | ------------------------------------------------ | | `in-progress` | Video is still being rendered | Continue polling every 10–30 seconds | | `completed` | Video is ready for download | Retrieve the video using the URL in the response | | `failed` | Rendering encountered an error | Review the error message and retry | **Tip:** Poll the status endpoint every 10–30 seconds. Polling too frequently may trigger rate limiting. ## Best Practices Follow these recommendations to use the API securely and efficiently: ### Keep Your API Key Safe * **Use environment variables** — Never hardcode your key directly in source code * **Never commit to Git** — Add your key file to `.gitignore` * **Use server-side code only** — Never expose your key in client-side applications * **Regenerate if compromised** — Generate a new key immediately if you suspect unauthorized access ### Handle Errors Gracefully * **Use try-catch blocks** — Wrap API calls to handle failures without crashing your application * **Implement exponential backoff** — On failure, retry with increasing delays (2s, 4s, 8s) * **Persist job IDs** — Store job IDs for status tracking and debugging * **Validate inputs** — Ensure all required fields are populated before making API calls ### Optimize Performance * **Poll at appropriate intervals** — Check job status every 10–30 seconds to avoid rate limiting * **Cache responses** — Store frequently accessed data locally to reduce redundant API calls * **Batch operations** — When creating multiple videos, process them concurrently rather than sequentially ## Troubleshooting Common Issues Below are common issues and their solutions: **Cause:** The API key is invalid, missing, or expired. **Resolution:** 1. Verify that your API key starts with `pictai_` 2. Confirm it is included in the `Authorization` header 3. Check that your subscription is active at the [API Access page](https://app.pictory.ai/api-access) 4. If the issue persists, generate a new API key from the [API Access page](https://app.pictory.ai/api-access) **Cause:** The specified voice name does not exist or is incorrectly formatted. **Resolution:** 1. Voice names are case-sensitive — `"Brian"` is valid, `"brian"` is not 2. Refer to the [complete list of available voices](/api-reference/voiceovers/get-voiceover-tracks) 3. Ensure the voice name matches exactly as documented **Cause:** A required field is missing or a parameter has an incorrect data type. **Resolution:** 1. Review the [API documentation](/api-reference/video-storyboard/update-storyboard-elements) for required fields 2. Ensure correct data types — strings in quotes, numbers without, booleans as `true`/`false` 3. Verify field name casing — e.g., `videoName` not `VideoName` **Cause:** API call frequency has exceeded the allowed rate. **Resolution:** 1. Increase the interval between status checks to 10–30 seconds 2. Implement exponential backoff — wait 2s, then 4s, then 8s between retries 3. Review your [subscription limits](https://app.pictory.ai/api-access) and upgrade if necessary **Cause:** The video is still rendering, or the job may have stalled. **Resolution:** 1. Allow sufficient time — videos typically take 5–15 minutes depending on length 2. Continue polling every 10–30 seconds 3. If the status has not changed after 30 minutes, [contact support](https://pictory.ai/contact) with your job ID ## Build Faster with an LLM Hand the entire Pictory API to Claude, ChatGPT, Cursor, or Windsurf in one shot — via `llms-full.txt`, the OpenAPI spec, or the Pictory MCP server. Includes a recommended system prompt and example natural-language → API-call mappings. **Quick access:** * **Full docs for LLMs:** [https://docs.pictory.ai/llms-full.txt](https://docs.pictory.ai/llms-full.txt) * **OpenAPI spec:** [https://docs.pictory.ai/openapi.json](https://docs.pictory.ai/openapi.json) * **End-to-end recipes:** [Common use cases with ready-to-run payloads](/guides/recipes/end-to-end-recipes) ## Next Steps Now that you are familiar with the fundamentals, explore these resources to build more advanced integrations: ### Popular Guides Learn how to turn any text into an engaging video Add natural-sounding AI narration to your videos Transform your blog posts into shareable videos Apply your logo, colors, and fonts automatically ### API Reference For complete technical details, refer to the API documentation: Complete reference for creating videos Monitor all your video creation jobs Manage subtitle styles and brand presets Auto-upload to AWS S3 and Vimeo *** ## Need Help? Ask questions and connect with other developers Get direct help from our support team ## Stay Connected Join our community and stay updated with the latest features, tips, and announcements: Get real-time updates, tips, and product announcements Professional insights, company news, and industry trends Visual inspiration, video tips, and creative content Video tutorials, feature demos, and how-to guides Join 20,000+ creators sharing tips and success stories Join our Reddit community for discussions and updates Chat with developers, get quick help, and share feedback # Text to Video with Make.com and Google Sheets Source: https://docs.pictory.ai/integrations/make/google-sheet-text-to-video-make-integration Automate video creation from Google Sheets using Pictory API and Make.com scenarios. A two-scenario approach designed for reliable execution within Make.com's free plan limits. This guide demonstrates how to build a no-code video creation pipeline using [Make.com](https://www.make.com) (formerly Integromat) and Google Sheets. The integration uses two separate Make.com scenarios to render videos from spreadsheet data and receive completion notifications via webhook. ## Why Two Scenarios? Make.com scenarios have a maximum execution time of **40 minutes on paid plans** and **5 minutes on the free plan**. Since video rendering can take several minutes, a single scenario that submits a render request and waits for completion could exceed these limits. By splitting the workflow into two scenarios, each scenario completes quickly and well within the free plan's 5-minute execution limit: 1. **Scenario 1** submits the render request and writes the job ID to the spreadsheet (completes in seconds) 2. **Scenario 2** receives a webhook callback when rendering finishes and updates the spreadsheet with the video URL (completes in seconds) This two-scenario design is ideal for **free plan users** and ensures reliable execution regardless of how long the video takes to render. ## Architecture Overview ```mermaid theme={null} flowchart LR subgraph Scenario1["Scenario 1: Render Video"] A[Watch New Rows] --> B[Search Approved Rows] B --> C[Pictory Render API] C --> D[Write Job ID] end subgraph Pictory["Pictory Cloud"] E[Video Rendering Engine] end subgraph Scenario2["Scenario 2: Webhook"] F[Webhook Trigger] --> G[Search Job Row] G --> H[Update Video URL] H --> I[Update Review Status] end C -->|render request + webhook URL| E E -->|webhook callback| F ``` ## Before You Begin Ensure you have the following: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * A [Make.com](https://www.make.com) account (free or paid) * A Google account with access to Google Sheets * A Google Sheets spreadsheet set up with the required structure (described below) ## Google Sheets Setup Create a Google Sheets spreadsheet with two sheets: **Videos** and **Video Trigger**. ### Sheet 1: "Videos" This is the primary data sheet that stores video entries and their rendering results. Set up the following column headers in row 1: | Column A | Column B | Column C | Column D | Column E | | -------------- | --------- | ---------- | ------------- | ----------------- | | **Video Name** | **Story** | **Job Id** | **Video Url** | **Review Status** | For each video entry, populate the following columns: * **Video Name**: A descriptive name for the video * **Story**: The full story text that Pictory will convert into video scenes * **Review Status**: Set to `approved` when the row is ready for rendering Leave the **Job Id** and **Video Url** columns empty — these are populated automatically by the scenarios during execution. Google Sheets Videos sheet with video data ready for rendering ### Sheet 2: "Video Trigger" This sheet controls when Scenario 1 runs. It contains a single column: | Column A | | -------------------- | | **Trigger DateTime** | Each time you add a new datetime value to this sheet, Scenario 1 detects the new row and initiates a rendering cycle. Add the current date and time to trigger a new execution. Google Sheets Video Trigger sheet with datetime entries ## Spreadsheet Data Flow As the two scenarios execute, the Videos sheet progresses through the following states: ### State 1: Ready for Rendering The row contains the video name, story text, and review status set to `approved`. The Job Id and Video Url columns are empty. Spreadsheet state before rendering ### State 2: Render Submitted (After Scenario 1) After Scenario 1 executes, it submits the render request to Pictory and writes the returned `jobId` into the **Job Id** column. The review status remains `approved` while the video is being rendered in the Pictory cloud. Spreadsheet state after render submission with job ID ### State 3: Rendering Complete (After Scenario 2) When Pictory finishes rendering the video, it sends a webhook callback to Scenario 2. The scenario locates the row by matching the job ID, writes the rendered **Video Url**, and updates the **Review Status** from `approved` to `done`. Spreadsheet state after rendering complete with video URL and done status ## Scenario 1: Render Text to Video from Google Spreadsheet This scenario watches for new rows in the trigger sheet, finds approved video entries, submits them to the Pictory Render API, and writes the job ID back to the spreadsheet. Make.com scenario for rendering video from Google Spreadsheet ### Module Breakdown #### 1. Trigger Video Render (Google Sheets: Watch New Rows) | Property | Value | | ------------ | ------------------------------------------- | | **Module** | `google-sheets:watchRows` | | **Sheet** | Video Trigger | | **Schedule** | Every 5 minutes | | **Purpose** | Detects new rows added to the trigger sheet | This module monitors the "Video Trigger" sheet for new rows. When a new datetime value is added to a row, the module detects it and triggers the scenario execution. The polling interval can be adjusted based on your requirements. *** #### 2. Search for Approved Rows (Google Sheets: Search Rows) | Property | Value | | ----------- | --------------------------------------------------- | | **Module** | `google-sheets:filterRows` | | **Sheet** | Videos | | **Filter** | Column E (Review Status) equals `approved` | | **Purpose** | Finds video entries that are approved for rendering | This module searches the "Videos" sheet for rows where the **Review Status** column contains `approved`. Only approved entries proceed to the rendering step. *** #### 3. Pictory Render Storyboard Video (HTTP: Make a Request) | Property | Value | | ----------- | --------------------------------------------------------------- | | **Module** | `http:MakeRequest` | | **Method** | `POST` | | **URL** | `https://api.pictory.ai/pictoryapis/v2/video/storyboard/render` | | **Purpose** | Submits the video rendering request to Pictory | This module sends a `POST` request to the Pictory [Render Storyboard Video](/api-reference/videos/render-storyboard-video) API with the following configuration: **Headers:** * `Content-Type`: `application/json` * `Authorization`: Your Pictory API key **Request Body:** ```json theme={null} { "videoName": "demo_text_to_video", "webhook": "YOUR_MAKE_WEBHOOK_URL", "smartLayoutName": "Wanderlust", "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "Brian" } ] }, "backgroundMusic": { "enabled": true, "volume": 0.1, "autoMusic": true }, "scenes": [ { "story": "{{story_from_spreadsheet}}", "createSceneOnNewLine": true, "createSceneOnEndOfSentence": true } ] } ``` Key fields: | Field | Description | | ----------------- | ------------------------------------------------------------------------------------------- | | `videoName` | Name assigned to the generated video | | `webhook` | The URL of your Scenario 2 webhook (Pictory sends a callback here when rendering completes) | | `smartLayoutName` | Visual layout theme applied to the video | | `voiceOver` | AI voice configuration with the selected speaker | | `backgroundMusic` | Auto-selected background music at 10% volume | | `scenes[].story` | The story text pulled from the spreadsheet's Story column | Replace `YOUR_MAKE_WEBHOOK_URL` with the actual webhook URL from Scenario 2. This URL is generated after creating the webhook trigger in Scenario 2. **Response:** Returns a `jobId` that uniquely identifies the rendering job. ```json theme={null} { "data": { "jobId": "abc123-def456-..." } } ``` *** #### 4. Write Job Id (Google Sheets: Update a Cell) | Property | Value | | ----------- | ------------------------------------------------------- | | **Module** | `google-sheets:updateCell` | | **Sheet** | Videos | | **Cell** | Column C (Job Id) at the matching row | | **Value** | `{{data.data.jobId}}` from the API response | | **Purpose** | Records the job ID for tracking and webhook correlation | This module writes the `jobId` returned by the Pictory API into the **Job Id** column of the corresponding row. This value is used by Scenario 2 to locate the correct row when the webhook fires. *** ## Scenario 2: Video Rendered Webhook This scenario listens for webhook callbacks from Pictory when a video finishes rendering, then updates the spreadsheet with the video URL and marks the row as completed. Make.com scenario for video rendered webhook ### Module Breakdown #### 1. Video Rendered Webhook (Custom Webhook) | Property | Value | | ----------- | ----------------------------------------------------------------- | | **Module** | `gateway:CustomWebHook` | | **Trigger** | Immediately as data arrives | | **Purpose** | Receives the callback from Pictory when video rendering completes | This module creates a custom webhook endpoint in Make.com. When Pictory finishes rendering a video, it sends a `POST` request to this webhook URL with the following payload: ```json theme={null} { "job_id": "abc123-def456-...", "success": true, "data": { "status": "completed", "progress": 100, "videoURL": "https://...", "videoShareURL": "https://...", "videoEmbedURL": "https://...", "audioURL": "https://...", "thumbnail": "https://...", "srtFile": "https://...", "txtFile": "https://...", "vttFile": "https://...", "videoDuration": 45.2, "encodingDuration": 30.5 }, "userId": "user-id" } ``` Copy the webhook URL generated by this module and paste it into the `webhook` field of the Pictory API request body in Scenario 1. *** #### 2. Search for Rendered Job Row (Google Sheets: Search Rows) | Property | Value | | ----------- | -------------------------------------------------------- | | **Module** | `google-sheets:filterRows` | | **Sheet** | Videos | | **Filter** | Column C (Job Id) equals `{{webhook.job_id}}` | | **Purpose** | Finds the spreadsheet row that matches the completed job | This module searches the "Videos" sheet for the row where the **Job Id** column matches the `job_id` received from the webhook. This correlates the webhook callback with the correct video entry. *** #### 3. Update Rendered Video Url (Google Sheets: Update a Cell) | Property | Value | | ----------- | ------------------------------------------------ | | **Module** | `google-sheets:updateCell` | | **Sheet** | Videos | | **Cell** | Column D (Video Url) at the matching row | | **Value** | `{{webhook.data.videoURL}}` | | **Purpose** | Writes the rendered video URL to the spreadsheet | Once the matching row is found, this module updates the **Video Url** column with the download URL of the rendered video. *** #### 4. Update Review Status (Google Sheets: Update a Cell) | Property | Value | | ----------- | -------------------------------------------- | | **Module** | `google-sheets:updateCell` | | **Sheet** | Videos | | **Cell** | Column E (Review Status) at the matching row | | **Value** | `done` | | **Purpose** | Marks the video entry as completed | This module updates the **Review Status** column from `approved` to `done`, indicating that the video has been successfully rendered and the URL is available. ## Import the Blueprints Both scenarios are available as pre-configured blueprint JSON files that can be imported directly into your Make.com account. Blueprints are Make.com's format for sharing scenario configurations — they contain the complete module setup, connections, and data mappings, eliminating the need to configure each module manually. Download the blueprint files before proceeding: * [Render Text To Video From Google Sheet](/integrations/make/Render%20Text%20To%20Video%20From%20Google%20Sheet.blueprint.json) (Scenario 1) * [Video Rendered Webhook](/integrations/make/Video%20Rendered%20Webhook.blueprint.json) (Scenario 2) Never share blueprint files containing your actual API key. Always use placeholder values like `YOUR_PICTORY_API_KEY` in shared blueprints and set the real key only in your private Make.com account. ### Step-by-Step Import and Configuration Follow this order to ensure the webhook URL is available when configuring Scenario 1. Scenario 2 must be created first because it generates the webhook URL that Scenario 1 needs. Create a new Google Sheets spreadsheet with two sheets named **Videos** and **Video Trigger**, using the column structure described in the [Google Sheets Setup](#google-sheets-setup) section above. Add at least one video entry to the "Videos" sheet with a video name, story text, and set the **Review Status** to `approved`. 1. Log in to your [Make.com](https://www.make.com) account 2. Click **Scenarios** in the left sidebar, then click **Create a new scenario** 3. In the scenario editor, click the **three-dot menu** (⋮) at the bottom of the screen 4. Select **Import Blueprint** from the menu 5. Choose the downloaded `Video Rendered Webhook.blueprint.json` file and click **Save** 6. The scenario modules will appear in the editor, matching the layout shown in the Scenario 2 screenshot above Each Google Sheets module in the scenario requires a connection to your Google account: 1. Click on the **Search for Rendered Job Row** module 2. Under **Connection**, click **Add** and sign in with your Google account to authorize Make.com 3. Select your spreadsheet under **Spreadsheet Name** and choose the **Videos** sheet 4. Repeat for the **Update Rendered Video Url** and **Update Review Status** modules, using the same Google connection 1. Click on the **Video Rendered Webhook** module (the first module in the scenario) 2. Under **Webhook**, click **Add** to create a new webhook 3. Assign a descriptive name (e.g., "Pictory Video Rendered Webhook") and click **Save** 4. Make.com generates a unique webhook URL. **Copy this URL** — it is required when configuring Scenario 1. Turn on Scenario 2 by toggling the **Scheduling** switch at the bottom of the editor. The scenario is set to trigger **Immediately as data arrives**, so it will automatically run whenever Pictory sends a webhook callback. 1. Go back to **Scenarios** and click **Create a new scenario** 2. Click the **three-dot menu** (⋮) at the bottom and select **Import Blueprint** 3. Choose the downloaded `Render Text To Video From Google Sheet.blueprint.json` file and click **Save** 1. Click on the **Trigger Video Render** module 2. Under **Connection**, add your Google account (or reuse the connection created in Scenario 2) 3. Select your spreadsheet and choose the **Video Trigger** sheet 4. Repeat for the **Search for Approved Rows** module (select the **Videos** sheet) and the **Write Job Id** module (select the **Videos** sheet) 1. Click on the **Pictory Render Storyboard Video** module (the HTTP request module) 2. In the **Headers** section, locate the **Authorization** header 3. Replace `YOUR_PICTORY_API_KEY` with your actual Pictory API key (e.g., `pictai_xxxx...`) 1. Still in the **Pictory Render Storyboard Video** module, scroll down to the **Request content** (JSON body) 2. Locate the `webhook` field in the JSON body 3. Replace `YOUR_MAKE_WEBHOOK_URL` with the webhook URL you copied from Scenario 2 in Step 4 1. Turn on Scenario 1 by toggling the **Scheduling** switch. The default schedule is set to run **Every 5 minutes**. 2. To initiate your first video render, add a datetime value (e.g., the current date and time) to a new row in the **Video Trigger** sheet 3. During the next polling cycle, Scenario 1 detects the new row, identifies approved entries in the Videos sheet, submits the render request, and writes the job ID back to the spreadsheet 4. Upon rendering completion, Pictory sends a webhook callback to Scenario 2, which updates the video URL and sets the review status to `done` After importing, click **Run once** at the bottom of the scenario editor to test each scenario before activating the automatic schedule. This verifies that all connections, spreadsheet references, and API credentials are configured correctly. ## Best Practices Store your Pictory API key securely. In Make.com, you can use the HTTP module's built-in authentication options or store keys in a data store. Avoid hardcoding API keys directly in shared blueprints. Use the **Webhooks** section in Make.com's left sidebar to monitor incoming webhook data. If a webhook fails to arrive, verify that the webhook URL in Scenario 1's request body matches the URL shown in Scenario 2's webhook module. The webhook payload includes a `success` field. You can add a **Router** module after the webhook trigger to handle failed renders separately, such as sending a notification or updating the spreadsheet with an error status. Scenario 1 processes one row per trigger execution. To render multiple videos, add multiple rows to the "Video Trigger" sheet. Each trigger cycle processes new rows and submits them for rendering. On the Make.com free plan, you are limited to two active scenarios and 1,000 operations per month. This two-scenario architecture fits within the free plan's scenario limit. Monitor your operation usage in the Make.com dashboard. ## Troubleshooting Verify your API key is correct in the HTTP request module's Authorization header. The value should be your complete API key (e.g., `pictai_xxxx...`). Do not add a `Bearer` prefix. Verify that Scenario 2 is active and the webhook URL in Scenario 1's request body exactly matches the URL generated in Scenario 2. Test by running Scenario 1 manually and reviewing Scenario 2's execution history. Confirm that all Google Sheets modules have a valid connection. Verify that the spreadsheet ID and sheet names match your Google Sheets document, and that column references (C, D, E) align with your spreadsheet structure. The two-scenario design prevents timeouts since each scenario completes in seconds. If timeouts persist, verify that large spreadsheets are not slowing the Search Rows module. Add filters to limit the number of rows processed. ## Next Steps Full API reference for the render endpoint used in Scenario 1 API reference for checking video rendering progress Learn about storyboard payload options and scene configuration Alternative workflow automation using n8n with polling-based approach # Text to Video n8n Workflow Integration Source: https://docs.pictory.ai/integrations/n8n/text-to-video-n8n-integration Automate text-to-video creation with Pictory API using n8n workflows — render storyboard videos, poll job status, and integrate with Google Sheets for batch processing This guide walks you through building an n8n workflow that integrates with the Pictory API to automate video creation. You'll learn how to render storyboard videos, poll for job completion with retry logic, and optionally read input from and write results back to a Google Sheets spreadsheet. ## Workflow Overview The n8n workflow automates the full Pictory video rendering pipeline: 1. Trigger the workflow (manually or from a spreadsheet) 2. Call the Pictory Render Storyboard Video API 3. Extract the `jobId` and initialize polling variables 4. Poll the job status every 30 seconds 5. Branch based on whether the job completed, failed, or is still in progress 6. Output the video URL on success, or handle failure/timeout n8n workflow for Pictory API integration ## Before You Begin Make sure you have: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * An [n8n](https://n8n.io/) instance (self-hosted or cloud) * Basic familiarity with n8n workflow editor ## Import the Workflow You can import the ready-made workflow JSON directly into n8n: 1. Open your n8n instance 2. Click **Add workflow** (or press `Ctrl+N`) 3. Click the **three-dot menu** (top-right) and select **Import from file...** 4. Upload the [n8n-workflow.json](/integrations/n8n/n8n-workflow.json) file 5. Replace `YOUR_PICTORY_API_KEY` with your actual Pictory API key in the **Pictory Render Storyboard Video** and **Get Job** nodes Never commit or share workflow files containing your actual API key. Always use placeholder values like `YOUR_PICTORY_API_KEY` in shared workflows and set the real key only in your private n8n instance. ## Node-by-Node Explanation ### 1. When clicking 'Execute workflow' (Manual Trigger) | Property | Value | | ----------- | ------------------------------ | | **Type** | `n8n-nodes-base.manualTrigger` | | **Purpose** | Starts the workflow on demand | This is the entry point of the workflow. When you click **Execute workflow** in the n8n editor, this node triggers and passes execution to the next node. In production, you can replace this with a **Schedule Trigger**, **Webhook Trigger**, or a **Google Sheets Trigger** to automate execution. *** ### 2. Pictory Render Storyboard Video (HTTP Request) | Property | Value | | ---------------------- | --------------------------------------------------------------------- | | **Type** | `n8n-nodes-base.httpRequest` | | **Method** | `POST` | | **URL** | `https://api.pictory.ai/pictoryapis/v2/video/storyboard/render` | | **Always Output Data** | `true` | | **Purpose** | Sends the storyboard payload to Pictory and initiates video rendering | This node makes a `POST` request to the Pictory [Render Storyboard Video](/api-reference/videos/render-storyboard-video) API. It sends: * **Headers**: `Content-Type: application/json` and `Authorization: YOUR_PICTORY_API_KEY` * **Body**: A JSON payload containing: * `videoName` — the name for the generated video * `smartLayoutName` — the visual layout theme (e.g., `"Wanderlust"`) * `voiceOver` — AI voice configuration with speaker name * `backgroundMusic` — auto-selected background music at 10% volume * `scenes` — the text story content, with `createSceneOnNewLine` and `createSceneOnEndOfSentence` both enabled to automatically split the story into multiple scenes **Response**: Returns a `jobId` in `data.jobId` that you use to track the rendering progress. ```json theme={null} { "data": { "jobId": "abc123-def456-..." } } ``` *** ### 3. Set JobId (Set Node) | Property | Value | | ---------------------- | ---------------------------------------------------- | | **Type** | `n8n-nodes-base.set` | | **Always Output Data** | `true` | | **Purpose** | Extracts the job ID and initializes polling counters | This node extracts three variables from the API response and stores them for use throughout the workflow: | Variable | Value | Description | | ------------ | ------------------------ | ------------------------------------------------- | | `jobId` | `{{ $json.data.jobId }}` | The rendering job identifier from Pictory | | `retryCount` | `0` | Current poll attempt counter (starts at 0) | | `maxRetries` | `30` | Maximum number of polling attempts before timeout | With a 30-second wait between polls and 30 max retries, the workflow will wait up to **15 minutes** for the video to render before timing out. *** ### 4. Wait for Job to Complete (Wait Node) | Property | Value | | ---------------------- | ---------------------------------------------- | | **Type** | `n8n-nodes-base.wait` | | **Wait Time** | `30` seconds | | **Always Output Data** | `true` | | **Purpose** | Pauses execution before polling the job status | This node introduces a 30-second delay before checking the job status. Video rendering takes time, so polling immediately after submission would return an `in-progress` status. This wait ensures the API has time to process. The Wait node pauses the entire workflow execution. In n8n, this means the execution is suspended and resumed after the wait period, so it does not consume resources while waiting. *** ### 5. Get Job (HTTP Request) | Property | Value | | ---------------------- | --------------------------------------------------------------------------------- | | **Type** | `n8n-nodes-base.httpRequest` | | **Method** | `GET` | | **URL** | `https://api.pictory.ai/pictoryapis/v1/jobs/{{ $('Set JobId').item.json.jobId }}` | | **Always Output Data** | `true` | | **Purpose** | Polls the Pictory Jobs API to check rendering status | This node calls the [Get Job](/api-reference/jobs/get-video-render-job-by-id) API using the `jobId` extracted earlier. The URL dynamically references the `jobId` from the **Set JobId** node using n8n's expression syntax `{{ $('Set JobId').item.json.jobId }}`. **Response**: Returns the job status along with video URLs when completed: ```json theme={null} { "data": { "status": "completed", "videoURL": "https://...", "videoShareURL": "https://..." } } ``` Possible status values: `in-progress`, `completed`, `failed`. *** ### 6. If Job is Completed or Failed (IF Node) | Property | Value | | ---------------------- | ---------------------------------------------------------------------- | | **Type** | `n8n-nodes-base.if` | | **Condition** | `data.status == "completed"` OR `data.status == "failed"` | | **Always Output Data** | `true` | | **Purpose** | Routes execution based on whether the job has reached a terminal state | This is the primary branching node: * **True branch** (status is `completed` or `failed`): Proceeds to the **If Job Completed** node for further evaluation * **False branch** (status is still `in-progress`): Proceeds to the **Increment Retry** node to poll again *** ### 7. If Job Completed (IF Node) | Property | Value | | ---------------------- | ------------------------------------------------------ | | **Type** | `n8n-nodes-base.if` | | **Condition** | `data.status == "completed"` | | **Always Output Data** | `true` | | **Purpose** | Distinguishes between a completed job and a failed job | This second-level branch runs only when the job has reached a terminal state: * **True branch** (completed): Routes to **Set Job Output** to extract video URLs * **False branch** (failed): Routes to **Set Failed Job Status** to record the failure *** ### 8. Set Job Output (Set Node) | Property | Value | | ---------------------- | ----------------------------------------------------- | | **Type** | `n8n-nodes-base.set` | | **Always Output Data** | `true` | | **Purpose** | Extracts video URLs from a successfully completed job | When the job completes successfully, this node extracts: | Variable | Value | Description | | --------------- | -------------------------------------- | ------------------------------------------ | | `videoUrl` | `{{ $json.data.videoURL }}` | Direct download URL for the rendered video | | `videoShareUrl` | `{{ $json.data.videoShareURL }}` | Shareable video URL | | `jobId` | `{{ $('Set JobId').item.json.jobId }}` | The original job ID for reference | This is the **success endpoint** of the workflow. You can connect additional nodes here to send notifications, upload the video, or write results back to a spreadsheet. *** ### 9. Set Failed Job Status (Set Node) | Property | Value | | ----------- | ----------------------------------- | | **Type** | `n8n-nodes-base.set` | | **Purpose** | Records a job failure with a reason | When the job status is `failed`, this node sets: | Variable | Value | | ------------------ | ------------------------ | | `status` | `"failed"` | | `reasonForFailure` | `"job execution failed"` | This is a **failure endpoint**. You can connect notification nodes here (e.g., Slack, email) to alert you when a video render fails. *** ### 10. Increment Retry (Set Node) | Property | Value | | ---------------------- | ------------------------------------------------- | | **Type** | `n8n-nodes-base.set` | | **Always Output Data** | `true` | | **Purpose** | Increments the retry counter for the polling loop | When the job is still in progress, this node: * Increments `retryCount` by 1: `{{ $('Set JobId').item.json.retryCount + 1 }}` * Preserves `jobId` and `maxRetries` from the original **Set JobId** node This creates the polling loop by updating the counter before checking whether to continue polling. *** ### 11. Can Poll Job (IF Node) | Property | Value | | ---------------------- | ------------------------------------------------------------------ | | **Type** | `n8n-nodes-base.if` | | **Condition** | `retryCount == maxRetries` | | **Always Output Data** | `true` | | **Purpose** | Guards against infinite polling by enforcing a maximum retry limit | This node checks if the retry limit has been reached: * **True branch** (retryCount equals maxRetries): The job has timed out. Routes to **Set Timeout Job Status** * **False branch** (retryCount is less than maxRetries): Loops back to **Wait for Job to Complete** to poll again *** ### 12. Set Timeout Job Status (Set Node) | Property | Value | | ---------------------- | --------------------------------------------------- | | **Type** | `n8n-nodes-base.set` | | **Always Output Data** | `true` | | **Purpose** | Records a timeout when polling exhausts all retries | When max retries are reached without the job completing, this node sets: | Variable | Value | | ------------------ | ------------------- | | `renderStatus` | `"failed"` | | `reasonForFailure` | `"job timed out"` | | `jobId` | The original job ID | This is the **timeout endpoint**. Consider increasing `maxRetries` in the **Set JobId** node if your videos consistently need more time to render. ## Understanding `alwaysOutputData` Most nodes in this workflow have `alwaysOutputData` set to `true`. This is a critical n8n node setting that controls what happens when a node produces no output items. **What it does:** When `alwaysOutputData` is enabled, the node will always output at least one empty item (`[{}]`), even if the node itself produces no data. Without this setting, a node that returns no items would cause downstream nodes to be skipped entirely. **Why it matters in this workflow:** * **Polling loop continuity**: The polling loop (Wait -> Get Job -> IF -> Increment Retry -> Can Poll Job -> back to Wait) must never break due to an empty output. If the **Get Job** node returned no data (e.g., due to a network issue), `alwaysOutputData` ensures the workflow still routes through the IF nodes and continues polling rather than silently stopping. * **IF node branching**: IF nodes with `alwaysOutputData` ensure that both the true and false branches always emit an item. This prevents the workflow from stalling when a condition does not match any input items. * **Error visibility**: Without `alwaysOutputData`, a node that fails silently (no output) would make the workflow appear to hang. With it enabled, the empty item propagates through the chain, making it easier to debug where things went wrong. As a best practice, enable `alwaysOutputData` on all nodes in polling/retry workflows. This ensures the loop never breaks silently and all branches always execute, giving you predictable behavior even when API responses are unexpected. ## Workflow Flow Diagram ```mermaid theme={null} flowchart TD A[Manual Trigger] --> B[Pictory Render Storyboard Video] B --> C[Set JobId + retryCount=0 + maxRetries=30] C --> D[Wait 30 seconds] D --> E[Get Job Status] E --> F{Job Completed or Failed?} F -->|Yes| G{Job Completed?} F -->|No - Still in progress| H[Increment Retry] G -->|Yes| I[Set Job Output: videoUrl, videoShareUrl] G -->|No - Failed| J[Set Failed Job Status] H --> K{retryCount == maxRetries?} K -->|Yes - Timed out| L[Set Timeout Job Status] K -->|No - Keep polling| D ``` ## Google Sheets Integration A common use case is reading video content from a Google Sheets spreadsheet, rendering videos for each row, and writing the results (job ID and video URL) back to the spreadsheet. ### Spreadsheet Structure Set up your Google Sheet with the following columns: | Column A | Column B | Column C | Column D | Column E | | -------------------------- | -------------- | ---------------------- | ---------------------- | ---------------------- | | **Story Text** | **Video Name** | **Job ID** | **Status** | **Video URL** | | Your story content here... | my\_video\_1 | *(filled by workflow)* | *(filled by workflow)* | *(filled by workflow)* | ### Modified Workflow for Spreadsheet Integration To integrate with Google Sheets, modify the workflow with these additional nodes: #### Step 1: Read from Google Sheets (Replace Manual Trigger) Replace the **Manual Trigger** node with a **Google Sheets** node: * **Operation**: Read Rows * **Document**: Select your spreadsheet * **Sheet**: Select the sheet name * **Options**: Filter rows where **Job ID** column is empty (to only process new rows) Each row becomes an item that flows through the workflow. For example, if you have 20 rows, the workflow processes 20 videos. #### Step 2: Use Spreadsheet Data in the Render Request In the **Pictory Render Storyboard Video** node, replace the hardcoded body with dynamic expressions referencing the spreadsheet columns: ```json theme={null} { "videoName": "={{ $json['Video Name'] }}", "smartLayoutName": "Wanderlust", "voiceOver": { "enabled": true, "aiVoices": [{ "speaker": "Brian" }] }, "backgroundMusic": { "enabled": true, "volume": 0.1, "autoMusic": true }, "scenes": [ { "story": "={{ $json['Story Text'] }}", "createSceneOnNewLine": true, "createSceneOnEndOfSentence": true } ] } ``` #### Step 3: Write Job ID to Spreadsheet After the **Set JobId** node, add a **Google Sheets** node to write the job ID back: * **Operation**: Update Row * **Document**: Select your spreadsheet * **Sheet**: Select the sheet name * **Mapping Column**: Use the row number or a unique identifier * **Values to Update**: * **Job ID** column: `{{ $json.jobId }}` * **Status** column: `in-progress` This immediately records the job ID so you can track which rows have been submitted for rendering. #### Step 4: Write Video URL to Spreadsheet on Completion After the **Set Job Output** node (success endpoint), add another **Google Sheets** node: * **Operation**: Update Row * **Document**: Select your spreadsheet * **Sheet**: Select the sheet name * **Matching Column**: Match on the **Job ID** column with value `{{ $json.jobId }}` * **Values to Update**: * **Status** column: `completed` * **Video URL** column: `{{ $json.videoUrl }}` Use the **Job ID** column as the matching key to ensure the video URL is written to the correct row. The Google Sheets node's **Update Row** operation can match on any column value. It finds the row where the Job ID matches and updates the corresponding Video URL column. #### Step 5: Write Failure Status to Spreadsheet Similarly, after the **Set Failed Job Status** and **Set Timeout Job Status** nodes, add Google Sheets nodes to update the status: * **Matching Column**: Match on the **Job ID** column * **Values to Update**: * **Status** column: `failed` or `timed out` * **Video URL** column: *(leave empty)* ### Complete Spreadsheet Workflow Diagram ```mermaid theme={null} flowchart TD A[Google Sheets: Read Rows] --> B[Pictory Render Storyboard Video] B --> C[Set JobId] C --> W[Google Sheets: Write Job ID + Status=in-progress] W --> D[Wait 30 seconds] D --> E[Get Job Status] E --> F{Completed or Failed?} F -->|Yes| G{Completed?} F -->|No| H[Increment Retry] G -->|Yes| I[Set Job Output] I --> S1[Google Sheets: Write Video URL + Status=completed] G -->|No| J[Set Failed Status] J --> S2[Google Sheets: Write Status=failed] H --> K{Max retries?} K -->|Yes| L[Set Timeout Status] L --> S3[Google Sheets: Write Status=timed out] K -->|No| D ``` ## Best Practices Use n8n's **Credentials** feature or **Environment Variables** to store your Pictory API key instead of hardcoding it in HTTP Request nodes. This prevents accidental exposure when sharing or exporting workflows. The default configuration polls every 30 seconds with a maximum of 30 retries (15 minutes total). For longer videos, increase `maxRetries` in the **Set JobId** node. The recommended polling interval is 10–30 seconds. If processing many videos in batch (e.g., from a large spreadsheet), add a short delay between render requests to avoid hitting Pictory API rate limits. You can use a **Wait** node with a 2-5 second delay before the Render node. Connect the failure endpoints (**Set Failed Job Status** and **Set Timeout Job Status**) to notification nodes such as Slack, email, or Discord to get alerted when a render fails or times out. Filter spreadsheet rows by checking if the **Job ID** column is empty before processing. This ensures rows that have already been submitted are not processed again if the workflow is re-run. ## Troubleshooting Verify your API key is correct and includes the full key string. The Authorization header value should be your complete API key (e.g., `pictai_xxxx...`). Do not add `Bearer` prefix. Increase `maxRetries` in the **Set JobId** node. Longer stories or higher-quality renders may take more than 15 minutes. Also verify that the initial render request returned a valid `jobId`. Ensure the **Matching Column** is set correctly in the Google Sheets Update Row operation. The Job ID value must exactly match what was written earlier. Check that your Google Sheets credentials have write permissions. In n8n, the Wait node suspends workflow execution. If you are using n8n in queue mode, ensure your workers are running. For the default main mode, the workflow resumes automatically after the wait period. ## Next Steps Full API reference for the render endpoint used in this workflow API reference for polling job status Learn about storyboard payload options and scene configuration Configure AI voices for your automated video pipeline # Embed Preview Player Source: https://docs.pictory.ai/integrations/storyboard-video-preview/embed-preview-player Integrate the Pictory storyboard preview player into your web application with real-time element updates This guide shows you how to embed the Pictory storyboard preview player in your web application using an iframe. You'll learn to display video previews, communicate with the player using postMessage, and update preview elements in real-time without re-rendering. ## Preview Player Demo Here's how the embedded preview player looks when integrated into your application: ``` ### Preview Editor Class Create a `preview-editor.js` file with the PreviewEditor class that handles iframe communication: ```javascript theme={null} import { v4 as uuid } from 'uuid'; export class PreviewEditor { constructor(containerElement, previewUrl, options = {}) { this.options = options; this.inProgressTasks = {}; this.loaded = false; this.onLoaded = null; this.onError = null; // Create iframe const iframe = document.createElement('iframe'); iframe.setAttribute('src', previewUrl); iframe.style.border = 'none'; iframe.style.height = '100%'; iframe.style.width = '100%'; iframe.allow = 'autoplay; fullscreen'; // Clear container and append iframe containerElement.innerHTML = ''; containerElement.appendChild(iframe); this.iframe = iframe; // Listen for messages from the iframe window.addEventListener('message', this.receiveEditorMessages); } // Clean up resources close() { window.removeEventListener('message', this.receiveEditorMessages); if (this.iframe.parentNode) { this.iframe.parentNode.removeChild(this.iframe); this.iframe.setAttribute('src', ''); } this.inProgressTasks = {}; } // Update preview elements updatePreview = async (renderParams) => { await this.executeEditorAction({ message: 'UPDATE_PREVIEW_ELEMENTS', elements: renderParams.elements }).catch(error => { throw new Error(`Failed to update preview: ${error.message}`); }); } // Execute action on the editor iframe executeEditorAction = async (message, payload) => { if (!this.loaded) { throw new Error('The Editor is not loaded.'); } const id = uuid(); if (this.iframe.contentWindow) { this.iframe.contentWindow.postMessage( { id, ...JSON.parse(JSON.stringify(message)), ...payload }, '*' ); } // Return promise that resolves when iframe responds return new Promise((resolve, reject) => { this.inProgressTasks[id] = { resolve, reject }; }); } // Handle messages from the iframe receiveEditorMessages = async (event) => { if (!event.data || typeof event.data !== 'object') { return; } // Verify message is from our iframe if (this.iframe.contentWindow !== event.source) { return; } const { id, message, error, ...args } = event.data; if (id) { // Resolve pending promise const inProgressTask = this.inProgressTasks[id]; if (inProgressTask) { if (error) { inProgressTask.reject(new Error(error)); } else { inProgressTask.resolve(args); } delete this.inProgressTasks[id]; } } else { // Handle broadcast messages switch (message) { case 'ON_LOADED': this.loaded = true; if (this.onLoaded) { await this.onLoaded(this); } break; case 'ON_ERROR': this.errored = true; if (this.onError) { await this.onError(this, error); } break; } } } } ``` ## Step 4: React Integration Here's a complete React component for embedding the preview player: ```jsx theme={null} import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { PreviewEditor } from './previewEditor'; const PreviewPlayer = ({ previewUrl, renderParams, onLoaded, onError }) => { const containerRef = useRef(null); const [editor, setEditor] = useState(null); const [isLoading, setIsLoading] = useState(true); // Initialize preview editor on mount useLayoutEffect(() => { if (!containerRef.current || !previewUrl) return; const previewEditor = new PreviewEditor( containerRef.current, previewUrl ); // Handle loaded event previewEditor.onLoaded = async (editor) => { setIsLoading(false); onLoaded?.(editor); }; // Handle error event previewEditor.onError = (editor, error) => { setIsLoading(false); onError?.(error); }; setEditor(previewEditor); // Cleanup on unmount return () => { previewEditor.close(); }; }, [previewUrl]); // Update preview when renderParams change useEffect(() => { if (editor && renderParams && editor.loaded) { editor.updatePreview(renderParams); } }, [renderParams, editor]); return (
{isLoading && (
Loading preview...
)}
); }; export default PreviewPlayer; ``` ### Usage in Your App ```jsx theme={null} import React, { useState, useEffect } from 'react'; import PreviewPlayer from './PreviewPlayer'; import { createStoryboardPreview, getPreviewData } from './api'; function App() { const [previewUrl, setPreviewUrl] = useState(null); const [renderParams, setRenderParams] = useState(null); const [isGenerating, setIsGenerating] = useState(false); const generatePreview = async () => { setIsGenerating(true); try { const jobId = await createStoryboardPreview(); const data = await getPreviewData(jobId); setPreviewUrl(data.previewUrl); setRenderParams(data.renderParams); } catch (error) { console.error('Failed to generate preview:', error); } finally { setIsGenerating(false); } }; // Find and update background element by ID const updateBackgroundVisual = (elementId, newVisualUrl, visualType = 'video') => { const updatedElements = renderParams.elements.map(element => { if (element.id === elementId && element.elementType === 'backgroundElement') { return { ...element, url: newVisualUrl, visualUrl: newVisualUrl, type: visualType, visualType: visualType }; } return element; }); // Update renderParams to trigger preview update setRenderParams({ ...renderParams, elements: updatedElements }); }; // Get background elements from renderParams const backgroundElements = renderParams?.elements?.filter( el => el.elementType === 'backgroundElement' ) || []; return (
{previewUrl && (
console.log('Preview loaded!')} onError={(err) => console.error('Preview error:', err)} />
)} {backgroundElements.length > 0 && (

Change Background Visual:

{backgroundElements.map((element, index) => (
updateBackgroundVisual(element.id, e.target.value)} style={{ width: '100%', padding: '10px', marginTop: '5px' }} /> Current: {element.visualDescription || 'No description'}
))}
)}
); } export default App; ``` ## Step 5: Update Preview Elements in Real-time The key feature is updating preview elements without re-rendering the entire video. When you modify the `elements` array and call `updatePreview()`, the player updates instantly. ### Replacing Background Visual To replace a scene's background video or image, find the `backgroundElement` and update its `url` and `visualUrl` properties: ```javascript theme={null} // Get the current elements from renderParams const elements = [...renderParams.elements]; // Find the background element you want to update const backgroundElement = elements.find( el => el.id === 'backgroundElement_1' && el.elementType === 'backgroundElement' ); if (backgroundElement) { // Update the visual URL (both url and visualUrl should be updated) backgroundElement.url = "https://your-cdn.com/new-background-video.mp4"; backgroundElement.visualUrl = "https://your-cdn.com/new-background-video.mp4"; } // Update the preview await previewEditor.updatePreview({ elements }); ``` ### Background Element Properties | Property | Type | Description | Example | | ----------------- | ------- | ------------------------------ | -------------------------------- | | `url` | string | Primary media URL | `"https://cdn.com/video.mp4"` | | `visualUrl` | string | Visual media URL (same as url) | `"https://cdn.com/video.mp4"` | | `type` | string | Media type | `"video"` or `"image"` | | `visualType` | string | Visual media type | `"video"` or `"image"` | | `duration` | number | Duration in seconds | `6.86` | | `loop` | boolean | Loop video playback | `true` | | `mute` | boolean | Mute video audio | `true` | | `backgroundColor` | string | Fallback color | `"rgba(255,255,255,1)"` | | `objectMode` | string | Media fit mode | `"cover"`, `"contain"`, `"fill"` | ### Example: Building a Background Visual Editor ```jsx theme={null} const BackgroundVisualEditor = ({ elements, onElementsChange }) => { // Filter only background elements const backgroundElements = elements.filter( el => el.elementType === 'backgroundElement' ); const updateBackgroundElement = (elementId, updates) => { const newElements = elements.map(element => { if (element.id === elementId) { return { ...element, ...updates }; } return element; }); onElementsChange(newElements); }; return (

Background Visuals

{backgroundElements.map((element, index) => (

Scene {index + 1} Background

{/* Visual URL Editor */}
{ updateBackgroundElement(element.id, { url: e.target.value, visualUrl: e.target.value }); }} placeholder="Enter video or image URL" style={{ width: '100%', padding: '8px', marginTop: '4px' }} />
{/* Visual Type Selector */}
))}
); }; ``` ### Replacing Multiple Backgrounds ```javascript theme={null} // Replace all background visuals with new URLs from an array function replaceAllBackgrounds(elements, newVisuals) { let visualIndex = 0; return elements.map(element => { if (element.elementType === 'backgroundElement' && newVisuals[visualIndex]) { const newVisual = newVisuals[visualIndex]; visualIndex++; return { ...element, url: newVisual.url, visualUrl: newVisual.url, type: newVisual.type || 'video', visualType: newVisual.type || 'video', visualDescription: newVisual.description || element.visualDescription }; } return element; }); } // Usage const newVisuals = [ { url: 'https://cdn.com/scene1-background.mp4', type: 'video', description: 'Intro animation' }, { url: 'https://cdn.com/scene2-background.jpg', type: 'image', description: 'Product shot' }, { url: 'https://cdn.com/scene3-background.mp4', type: 'video', description: 'Demo footage' } ]; const updatedElements = replaceAllBackgrounds(renderParams.elements, newVisuals); await previewEditor.updatePreview({ elements: updatedElements }); ``` ## PostMessage API Reference ### Messages Sent to the Preview Player | Message | Payload | Description | | ------------------------- | ------------------ | ---------------------------------------- | | `UPDATE_PREVIEW_ELEMENTS` | `{ elements: [] }` | Update the preview with new element data | ### Messages Received from the Preview Player | Message | Payload | Description | | ----------- | ------------------- | ---------------------------------------- | | `ON_LOADED` | None | Player has finished loading and is ready | | `ON_ERROR` | `{ error: string }` | An error occurred in the player | ### Message Format All messages follow this structure: ```javascript theme={null} // Outgoing message (to player) { id: "unique-message-id", // For tracking response message: "UPDATE_PREVIEW_ELEMENTS", elements: [...] } // Incoming message (from player) { id: "unique-message-id", // Matches outgoing message // ... response data } // Broadcast message (from player, no id) { message: "ON_LOADED" } ``` ## Complete Integration Example Here's a complete end-to-end example: ```javascript Complete Example theme={null} import axios from 'axios'; import { v4 as uuid } from 'uuid'; // ============================================ // API Functions // ============================================ const API_BASE_URL = 'https://api.pictory.ai/pictoryapis'; const API_KEY = 'YOUR_API_KEY'; async function createPreview(story) { const response = await axios.post( `${API_BASE_URL}/v2/video/storyboard`, { videoName: 'embedded_preview_demo', voiceOver: { enabled: true, aiVoices: [{ speaker: 'Brian', speed: 100 }] }, scenes: [{ story, createSceneOnEndOfSentence: true }] }, { headers: { Authorization: API_KEY, 'Content-Type': 'application/json' } } ); return response.data.data.jobId; } async function waitForPreview(jobId) { while (true) { const response = await axios.get( `${API_BASE_URL}/v1/jobs/${jobId}`, { headers: { Authorization: API_KEY } } ); const { status, previewUrl, renderParams } = response.data.data; if (status === 'completed') { return { previewUrl, renderParams }; } else if (status === 'failed') { throw new Error('Preview generation failed'); } await new Promise(resolve => setTimeout(resolve, 3000)); } } // ============================================ // Preview Editor Class // ============================================ class PreviewEditor { constructor(container, url) { this.inProgressTasks = {}; this.loaded = false; const iframe = document.createElement('iframe'); iframe.src = url; iframe.style.cssText = 'border:none;width:100%;height:100%'; iframe.allow = 'autoplay; fullscreen'; container.innerHTML = ''; container.appendChild(iframe); this.iframe = iframe; window.addEventListener('message', this.handleMessage); } close() { window.removeEventListener('message', this.handleMessage); this.iframe?.remove(); } async updatePreview(renderParams) { if (!this.loaded) throw new Error('Not loaded'); const id = uuid(); this.iframe.contentWindow.postMessage({ id, message: 'UPDATE_PREVIEW_ELEMENTS', elements: renderParams.elements }, '*'); return new Promise((resolve, reject) => { this.inProgressTasks[id] = { resolve, reject }; }); } handleMessage = (event) => { if (event.source !== this.iframe?.contentWindow) return; const { id, message, error } = event.data || {}; if (id && this.inProgressTasks[id]) { const task = this.inProgressTasks[id]; error ? task.reject(new Error(error)) : task.resolve(event.data); delete this.inProgressTasks[id]; } else if (message === 'ON_LOADED') { this.loaded = true; this.onLoaded?.(this); } else if (message === 'ON_ERROR') { this.onError?.(this, error); } }; } // ============================================ // Usage // ============================================ async function initializePreview() { const container = document.getElementById('preview-container'); const statusEl = document.getElementById('status'); statusEl.textContent = 'Creating preview...'; // Create and wait for preview const jobId = await createPreview( 'Welcome to our product demo. Discover amazing features. Start your journey today.' ); statusEl.textContent = 'Generating preview...'; const { previewUrl, renderParams } = await waitForPreview(jobId); statusEl.textContent = 'Loading player...'; // Initialize editor const editor = new PreviewEditor(container, previewUrl); editor.onLoaded = () => { statusEl.textContent = 'Preview ready!'; // Store for later updates window.previewEditor = editor; window.renderParams = renderParams; }; editor.onError = (_, error) => { statusEl.textContent = `Error: ${error}`; }; } // Update background visual with new URL function updateBackgroundVisual(elementId, newUrl, visualType = 'video') { const { previewEditor, renderParams } = window; // Find and update the background element const updatedElements = renderParams.elements.map(element => { if (element.id === elementId && element.elementType === 'backgroundElement') { return { ...element, url: newUrl, visualUrl: newUrl, type: visualType, visualType: visualType }; } return element; }); renderParams.elements = updatedElements; previewEditor.updatePreview(renderParams); } // Example: Replace first background with a new video function replaceFirstBackground() { updateBackgroundVisual( 'backgroundElement_1', 'https://your-cdn.com/new-product-video.mp4', 'video' ); } // Start initialization initializePreview(); ``` ## Best Practices Always show a loading indicator while the preview is initializing. The player may take a few seconds to load depending on network conditions. ```jsx theme={null} {isLoading && }
{/* Preview container */}
```
Always call `close()` when unmounting the component or navigating away to prevent memory leaks and remove event listeners. ```jsx theme={null} useEffect(() => { return () => { editor?.close(); }; }, [editor]); ``` When building live editors, debounce updates to prevent overwhelming the player with rapid changes. ```javascript theme={null} import { debounce } from 'lodash'; const debouncedUpdate = debounce((elements) => { editor.updatePreview({ elements }); }, 300); ``` Implement comprehensive error handling for network failures and player errors. ```jsx theme={null} editor.onError = (_, error) => { console.error('Player error:', error); showErrorNotification('Failed to load preview. Please try again.'); }; ```
## Troubleshooting **Problem:** The iframe shows a blank screen or loading forever. **Solutions:** * Verify the `previewUrl` is valid and not expired * Check browser console for CORS or security errors * Verify your domain is allowed to embed the preview **Problem:** Calling `updatePreview()` does not change the preview. **Solutions:** * Ensure `editor.loaded` is `true` before calling update * Check that elements array structure is correct * Verify the message is being received (check browser console) * Wait for the `ON_LOADED` event before updating **Problem:** Messages are not being received by the iframe. **Solutions:** * Verify `iframe.contentWindow` is not null * Check that you are listening for messages correctly * Ensure the message origin matches expected sources * Use browser dev tools to inspect postMessage traffic **Problem:** Application becomes slow after multiple preview loads. **Solutions:** * Always call `editor.close()` when done * Remove event listeners in cleanup functions * Clear the iframe src before removing * Use React's useEffect cleanup or componentWillUnmount ## Next Steps Convert the preview to a final rendered video Apply modifications and render the final video Full API reference for preview creation Monitor job progress and retrieve results # Text to Video with Zapier and Google Sheets Source: https://docs.pictory.ai/integrations/zapier/google-sheet-text-to-video-zapier-integration Automate video creation from Google Sheets using Pictory API and Zapier. A two-zap approach that keeps each zap lightweight and ensures reliable execution. This guide demonstrates how to build a no-code video creation pipeline using [Zapier](https://zapier.com) and Google Sheets. The integration uses two separate Zaps to render videos from spreadsheet data and receive completion notifications via webhook. ## Why Two Zaps? Zapier tasks have execution time limits that vary by plan. Since video rendering can take several minutes, a single Zap that submits a render request and waits for completion could exceed these limits or consume unnecessary task credits. By splitting the workflow into two Zaps, each Zap completes quickly: 1. **Zap 1** submits the render request and writes the job ID to the spreadsheet (completes in seconds) 2. **Zap 2** receives a webhook callback when rendering finishes and updates the spreadsheet with the video URL (completes in seconds) This two-zap design ensures reliable execution regardless of how long the video takes to render, and minimizes the number of Zapier task executions consumed. ## Architecture Overview ```mermaid theme={null} flowchart LR subgraph Zap1["Zap 1: Render Video"] A[Watch New Rows] --> B[Search Approved Rows] B --> C[Loop Through Rows] C --> D[Pictory Render API] D --> E[Write Job ID] end subgraph Pictory["Pictory Cloud"] F[Video Rendering Engine] end subgraph Zap2["Zap 2: Webhook"] G[Webhook Trigger] --> H[Search Job Row] H --> I[Update Video URL & Status] end D -->|render request + webhook URL| F F -->|webhook callback| G ``` ## Before You Begin Ensure you have the following: * A Pictory API key ([get one here](https://app.pictory.ai/api-access)) * A [Zapier](https://zapier.com) account (free or paid) * A Google account with access to Google Sheets * A Google Sheets spreadsheet set up with the required structure (described below) ## Google Sheets Setup Create a Google Sheets spreadsheet with two sheets: **Videos** and **Video Trigger**. ### Sheet 1: "Videos" This is the primary data sheet that stores video entries and their rendering results. Set up the following column headers in row 1: | Column A | Column B | Column C | Column D | Column E | | -------------- | --------- | ---------- | ------------- | ----------------- | | **Video Name** | **Story** | **Job Id** | **Video Url** | **Review Status** | For each video entry, populate the following columns: * **Video Name**: A descriptive name for the video * **Story**: The full story text that Pictory will convert into video scenes * **Review Status**: Set to `approved` when the row is ready for rendering Leave the **Job Id** and **Video Url** columns empty — these are populated automatically by the Zaps during execution. Google Sheets Videos sheet with video data ready for rendering ### Sheet 2: "Video Trigger" This sheet controls when Zap 1 runs. It contains a single column: | Column A | | -------------------- | | **Trigger DateTime** | Each time you add a new datetime value to this sheet, Zap 1 detects the new row and initiates a rendering cycle. Add the current date and time to trigger a new execution. Google Sheets Video Trigger sheet with datetime entries ## Spreadsheet Data Flow As the two Zaps execute, the Videos sheet progresses through the following states: ### State 1: Ready for Rendering The row contains the video name, story text, and review status set to `approved`. The Job Id and Video Url columns are empty. Spreadsheet state before rendering ### State 2: Render Submitted (After Zap 1) After Zap 1 executes, it submits the render request to Pictory and writes the returned `jobId` into the **Job Id** column. The review status remains `approved` while the video is being rendered in the Pictory cloud. Spreadsheet state after render submission with job ID ### State 3: Rendering Complete (After Zap 2) When Pictory finishes rendering the video, it sends a webhook callback to Zap 2. The Zap locates the row by matching the job ID, writes the rendered **Video Url**, and updates the **Review Status** from `approved` to `done`. Spreadsheet state after rendering complete with video URL and done status ## Zap 1: Render Text to Video from Google Spreadsheet This Zap watches for new rows in the trigger sheet, finds approved video entries, loops through each one, submits them to the Pictory Render API, and writes the job ID back to the spreadsheet. Zapier Zap for rendering video from Google Spreadsheet ### Step Breakdown #### 1. Trigger Video Render (Google Sheets: New or Updated Spreadsheet Row) | Property | Value | | ----------- | ------------------------------------------- | | **App** | Google Sheets | | **Trigger** | New or Updated Spreadsheet Row | | **Sheet** | Video Trigger | | **Purpose** | Detects new rows added to the trigger sheet | This step monitors the "Video Trigger" sheet for new rows. When a new datetime value is added to a row, Zapier detects it and triggers the Zap execution. Zapier automatically polls for new rows based on your plan's polling interval. *** #### 2. Search for Approved Rows (Google Sheets: Lookup Spreadsheet Row) | Property | Value | | ----------------- | --------------------------------------------------- | | **App** | Google Sheets | | **Action** | Lookup Spreadsheet Row | | **Sheet** | Videos | | **Lookup Column** | Column E (Review Status) | | **Lookup Value** | `approved` | | **Purpose** | Finds video entries that are approved for rendering | This step searches the "Videos" sheet for rows where the **Review Status** column contains `approved`. Only approved entries proceed to the rendering step. The search returns up to 10 matching rows from the bottom of the sheet. *** #### 3. Create Loop From Line Items (Looping by Zapier) | Property | Value | | ---------------- | ----------------------------------------------- | | **App** | Looping by Zapier | | **Action** | Create Loop From Line Items | | **Input Values** | Story, Row Number, Video Name, Review Status | | **Purpose** | Iterates through each approved row individually | This step takes the grouped search results from the previous step and loops through each matched row one at a time. For each iteration, it passes the story text, row number, video name, and review status to the subsequent steps. This ensures that each approved video entry is processed individually through the Pictory API. *** #### 4. Pictory Render Storyboard Video (Webhooks by Zapier: Custom Request) | Property | Value | | ----------- | --------------------------------------------------------------- | | **App** | Webhooks by Zapier | | **Action** | Custom Request | | **Method** | `POST` | | **URL** | `https://api.pictory.ai/pictoryapis/v2/video/storyboard/render` | | **Purpose** | Submits the video rendering request to Pictory | This step sends a `POST` request to the Pictory [Render Storyboard Video](/api-reference/videos/render-storyboard-video) API with the following configuration: **Headers:** * `Content-Type`: `application/json` * `Authorization`: Your Pictory API key **Request Body:** ```json theme={null} { "videoName": "demo_text_to_video", "webhook": "YOUR_ZAP_2_WEBHOOK_URL", "smartLayoutName": "Wanderlust", "voiceOver": { "enabled": true, "aiVoices": [ { "speaker": "Brian" } ] }, "backgroundMusic": { "enabled": true, "volume": 0.1, "autoMusic": true }, "scenes": [ { "story": "{{story_from_spreadsheet}}", "createSceneOnNewLine": true, "createSceneOnEndOfSentence": true } ] } ``` Key fields: | Field | Description | | ----------------- | -------------------------------------------------------------------------------------- | | `videoName` | Name assigned to the generated video | | `webhook` | The URL of your Zap 2 webhook (Pictory sends a callback here when rendering completes) | | `smartLayoutName` | Visual layout theme applied to the video | | `voiceOver` | AI voice configuration with the selected speaker | | `backgroundMusic` | Auto-selected background music at 10% volume | | `scenes[].story` | The story text pulled from the spreadsheet's Story column via the Looping step | Replace `YOUR_ZAP_2_WEBHOOK_URL` with the actual webhook URL from Zap 2. This URL is generated after creating the webhook trigger in Zap 2. **Response:** Returns a `jobId` that uniquely identifies the rendering job. ```json theme={null} { "data": { "jobId": "abc123-def456-..." } } ``` *** #### 5. Write Job Id (Google Sheets: Update Spreadsheet Row) | Property | Value | | ------------ | ------------------------------------------------------- | | **App** | Google Sheets | | **Action** | Update Spreadsheet Row | | **Sheet** | Videos | | **Row** | The matching row number from the Looping step | | **Column C** | `{{data.jobId}}` from the API response | | **Purpose** | Records the job ID for tracking and webhook correlation | This step writes the `jobId` returned by the Pictory API into the **Job Id** column of the corresponding row. This value is used by Zap 2 to locate the correct row when the webhook fires. The row is updated in place, preserving all existing column values. *** ## Zap 2: Video Rendered Webhook This Zap listens for webhook callbacks from Pictory when a video finishes rendering, then updates the spreadsheet with the video URL and marks the row as completed. Zapier Zap for video rendered webhook ### Step Breakdown #### 1. Video Rendered Webhook (Webhooks by Zapier: Catch Hook) | Property | Value | | ----------- | ----------------------------------------------------------------- | | **App** | Webhooks by Zapier | | **Trigger** | Catch Hook | | **Purpose** | Receives the callback from Pictory when video rendering completes | This step creates a webhook endpoint in Zapier. When Pictory finishes rendering a video, it sends a `POST` request to this webhook URL with the following payload: ```json theme={null} { "job_id": "abc123-def456-...", "success": true, "data": { "status": "completed", "progress": 100, "videoURL": "https://...", "videoShareURL": "https://...", "videoEmbedURL": "https://...", "audioURL": "https://...", "thumbnail": "https://...", "srtFile": "https://...", "txtFile": "https://...", "vttFile": "https://...", "videoDuration": 45.2, "encodingDuration": 30.5 }, "userId": "user-id" } ``` Copy the webhook URL generated by this step and paste it into the `webhook` field of the Pictory API request body in Zap 1. *** #### 2. Search for Rendered Job Row (Google Sheets: Lookup Spreadsheet Row) | Property | Value | | ----------------- | -------------------------------------------------------- | | **App** | Google Sheets | | **Action** | Lookup Spreadsheet Row | | **Sheet** | Videos | | **Lookup Column** | Column C (Job Id) | | **Lookup Value** | `{{job_id}}` from the webhook payload | | **Purpose** | Finds the spreadsheet row that matches the completed job | This step searches the "Videos" sheet for the row where the **Job Id** column matches the `job_id` received from the webhook. This correlates the webhook callback with the correct video entry. *** #### 3. Update Rendered Video Url (Google Sheets: Update Spreadsheet Row) | Property | Value | | ------------ | -------------------------------------------------------------- | | **App** | Google Sheets | | **Action** | Update Spreadsheet Row | | **Sheet** | Videos | | **Row** | The matching row from the search step | | **Column D** | `{{data.videoURL}}` from the webhook payload | | **Column E** | `done` | | **Purpose** | Writes the rendered video URL and marks the entry as completed | Once the matching row is found, this step updates the **Video Url** column with the download URL of the rendered video and sets the **Review Status** column from `approved` to `done`, indicating that the video has been successfully rendered and the URL is available. ## Import the Zaps Both Zaps are available as pre-configured JSON files that can be imported into your Zapier account via the Zapier Transfer tool. These files contain the complete step configuration, app connections, and data mappings. Download the Zap files before proceeding: * [Render Text To Video From Google Sheet](/integrations/zapier/Render%20Text%20To%20Video%20From%20Google%20Sheet%20Zap.json) (Zap 1) * [Pictory Video Rendered Webhook](/integrations/zapier/Pictory%20Video%20Rendered%20Webhook%20Zap.json) (Zap 2) Never share Zap files containing your actual API key. Always use placeholder values like `YOUR_PICTORY_API_KEY` in shared files and set the real key only in your private Zapier account. ### Step-by-Step Import and Configuration Follow this order to ensure the webhook URL is available when configuring Zap 1. Zap 2 must be created first because it generates the webhook URL that Zap 1 needs. Create a new Google Sheets spreadsheet with two sheets named **Videos** and **Video Trigger**, using the column structure described in the [Google Sheets Setup](#google-sheets-setup) section above. Add at least one video entry to the "Videos" sheet with a video name, story text, and set the **Review Status** to `approved`. 1. Log in to your [Zapier](https://zapier.com) account 2. Click **Create** in the top navigation bar and select **New Zap** 3. For the trigger, search for **Webhooks by Zapier** and select **Catch Hook** as the trigger event 4. Click **Continue** — Zapier generates a unique webhook URL. **Copy this URL** — it is required when configuring Zap 1 5. Optionally, click **Test trigger** to verify the webhook is active (you can send a test payload later) 1. Click the **+** button to add a new step 2. Search for **Google Sheets** and select **Lookup Spreadsheet Row** as the action 3. Connect your Google account when prompted 4. Select your spreadsheet and choose the **Videos** sheet 5. Set the **Lookup Column** to **Job Id** (Column C) 6. Map the **Lookup Value** to `{{job_id}}` from the webhook trigger data 1. Click the **+** button to add another step 2. Search for **Google Sheets** and select **Update Spreadsheet Row** as the action 3. Use the same Google Sheets connection 4. Select your spreadsheet and choose the **Videos** sheet 5. Set the **Row** field to the row number from the Lookup step 6. Map **Column D** (Video Url) to `{{data__videoURL}}` from the webhook payload 7. Set **Column E** (Review Status) to `done` Click **Publish** to activate Zap 2. The Zap is set to trigger instantly whenever Pictory sends a webhook callback. 1. Go back to the Zapier dashboard and click **Create** then **New Zap** 2. For the trigger, search for **Google Sheets** and select **New or Updated Spreadsheet Row** as the trigger event 3. Connect your Google account and select your spreadsheet 4. Choose the **Video Trigger** sheet and set the **Trigger Column** to **Trigger DateTime** 1. Add a new step with **Google Sheets** and select **Lookup Spreadsheet Row** 2. Select your spreadsheet and choose the **Videos** sheet 3. Set the **Lookup Column** to **Review Status** (Column E) 4. Set the **Lookup Value** to `approved` 1. Add a new step with **Looping by Zapier** and select **Create Loop From Line Items** 2. Map the input values from the search results: **Story**, **Row Number**, **Video Name**, and **Review Status** 3. This step iterates through each matched row individually for processing 1. Add a new step with **Webhooks by Zapier** and select **Custom Request** 2. Set the **Method** to `POST` 3. Set the **URL** to `https://api.pictory.ai/pictoryapis/v2/video/storyboard/render` 4. Add the following headers: * `Content-Type`: `application/json` * `Authorization`: Your Pictory API key (e.g., `pictai_xxxx...`) 5. Paste the JSON request body (shown in the [Pictory Render Storyboard Video](#4-pictory-render-storyboard-video-webhooks-by-zapier-custom-request) section above) into the **Data** field 6. Replace `YOUR_ZAP_2_WEBHOOK_URL` with the webhook URL you copied from Zap 2 in Step 2 7. Map the `story` field to the story value from the Looping step 1. Add a new step with **Google Sheets** and select **Update Spreadsheet Row** 2. Select your spreadsheet and choose the **Videos** sheet 3. Set the **Row** field to the row number from the Looping step 4. Map **Column C** (Job Id) to `{{data__jobId}}` from the Webhooks step response 5. Preserve the existing values for Columns A, B, and E by mapping them from the Looping step 1. Click **Publish** to activate Zap 1. Zapier will poll for new rows on the "Video Trigger" sheet at regular intervals based on your plan. 2. To initiate your first video render, add a datetime value (e.g., the current date and time) to a new row in the **Video Trigger** sheet 3. During the next polling cycle, Zap 1 detects the new row, identifies approved entries in the Videos sheet, submits the render request, and writes the job ID back to the spreadsheet 4. Upon rendering completion, Pictory sends a webhook callback to Zap 2, which updates the video URL and sets the review status to `done` After setting up each Zap, use Zapier's **Test** feature for each step to verify that all connections, spreadsheet references, and API credentials are configured correctly before publishing. ## Zapier vs Make.com: Key Differences If you are familiar with the [Make.com integration](/integrations/make/google-sheet-text-to-video-make-integration), note the following differences in the Zapier implementation: | Feature | Zapier | Make.com | | ----------------- | ------------------------------------------------------------- | ----------------------------------------------- | | **Terminology** | Zaps, Steps | Scenarios, Modules | | **Looping** | Requires explicit "Looping by Zapier" step | Built-in iterator handles arrays automatically | | **Webhook Setup** | "Webhooks by Zapier: Catch Hook" trigger | "Custom Webhook" module | | **HTTP Requests** | "Webhooks by Zapier: Custom Request" action | "HTTP: Make a Request" module | | **Row Updates** | Single "Update Spreadsheet Row" step updates multiple columns | Separate "Update a Cell" module for each column | | **Import Format** | JSON Zap files | Blueprint JSON files | ## Best Practices Store your Pictory API key securely. In Zapier, the key is entered directly into the Custom Request step's headers. Avoid sharing Zap configurations that contain your actual API key. Always replace it with a placeholder value before exporting. Use Zapier's **Zap History** to monitor incoming webhook data and step execution results. If a webhook fails to arrive, verify that the webhook URL in Zap 1's request body matches the URL shown in Zap 2's Catch Hook trigger. The webhook payload includes a `success` field. You can add a **Filter** step or **Paths** step after the webhook trigger to handle failed renders separately, such as sending an email notification or updating the spreadsheet with an error status. Zap 1 processes all approved rows in a single execution using the Looping step. Each loop iteration submits one video for rendering. To render multiple videos, simply add multiple rows to the "Videos" sheet with the review status set to `approved`, then add a trigger row to the "Video Trigger" sheet. On the Zapier free plan, you are limited to 5 single-step Zaps and 100 tasks per month. Since these Zaps use multiple steps, a paid plan is required. The Starter plan supports multi-step Zaps and provides 750 tasks per month. Monitor your task usage in the Zapier dashboard under **Settings > Usage**. ## Troubleshooting **Cause:** The API key in the Custom Request step's Authorization header is incorrect or missing. **Resolution:** 1. Open the Pictory Render Storyboard Video step in Zap 1 2. Verify the Authorization header contains your complete API key (e.g., `pictai_xxxx...`) 3. Do not add a `Bearer` prefix — use the API key directly **Cause:** The webhook URL in Zap 1's request body does not match the URL generated by Zap 2, or Zap 2 is not published. **Resolution:** 1. Confirm that Zap 2 is published and active 2. Open the Catch Hook trigger in Zap 2 and copy the webhook URL 3. Verify that the `webhook` field in the Pictory API request body in Zap 1 exactly matches this URL 4. Check Zap History for both Zaps to review execution logs **Cause:** The Google Sheets connection has expired, or the spreadsheet and sheet names do not match the configuration. **Resolution:** 1. Open each Google Sheets step and verify the connection is active 2. Reconnect your Google account if prompted 3. Confirm that the spreadsheet name and sheet names match your Google Sheets document 4. Verify that column references (C, D, E) align with your spreadsheet structure **Cause:** The search step is returning grouped results that the Looping step cannot parse, or the loop input values are not mapped correctly. **Resolution:** 1. Open the Search for Approved Rows step and verify it returns results in the expected format 2. In the Looping step, ensure each input value (Story, Row, Video Name, Review Status) is mapped to the correct field from the search results 3. Test the search step independently to confirm it returns the expected rows ## Next Steps Full API reference for the render endpoint used in Zap 1 API reference for checking video rendering progress Learn about storyboard payload options and scene configuration Alternative workflow automation using Make.com with the same two-scenario approach