From 6097a819d0bc73d7cfce17d046964f43c21c80cc Mon Sep 17 00:00:00 2001 From: Musab Date: Wed, 5 Aug 2026 04:46:35 +0500 Subject: [PATCH 1/4] feat(examples): add first-render quickstart (Node.js + Python) --- README.md | 1 + examples/first-render/.env.example | 1 + examples/first-render/.gitignore | 2 + examples/first-render/README.md | 51 ++++++++++++++ examples/first-render/edit.json | 31 +++++++++ examples/first-render/render.mjs | 107 +++++++++++++++++++++++++++++ examples/first-render/render.py | 100 +++++++++++++++++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 examples/first-render/.env.example create mode 100644 examples/first-render/.gitignore create mode 100644 examples/first-render/README.md create mode 100644 examples/first-render/edit.json create mode 100644 examples/first-render/render.mjs create mode 100644 examples/first-render/render.py diff --git a/README.md b/README.md index 3b68b18..6fb04b3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Clone this repo, or head into the folder for the example you want. Each example ## Examples +- [first-render](examples/first-render) submits an Edit, polls the render status, and prints the output URL, in Node.js and Python. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). - [instagram-ai-video](examples/instagram-ai-video) generates a script, voiceover and background image with AI, renders a 1080x1920 video, and publishes it as an Instagram Reel. Companion code for [How to automate Instagram posts with AI video](https://shotstack.io/learn/automate-instagram-posts-with-ai-video/). - [rapidreels](examples/rapidreels) creates faceless short-form videos using generative AI. [View demo](https://shotstack.io/demos/social-media-video-maker/). - [reelestate](examples/reelestate) turns static real estate images into fully edited video slideshows. [View demo](https://shotstack.io/demos/real-estate-video-listing-maker/). diff --git a/examples/first-render/.env.example b/examples/first-render/.env.example new file mode 100644 index 0000000..2bf0cb3 --- /dev/null +++ b/examples/first-render/.env.example @@ -0,0 +1 @@ +SHOTSTACK_API_KEY=your_sandbox_api_key diff --git a/examples/first-render/.gitignore b/examples/first-render/.gitignore new file mode 100644 index 0000000..713d500 --- /dev/null +++ b/examples/first-render/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env diff --git a/examples/first-render/README.md b/examples/first-render/README.md new file mode 100644 index 0000000..3065444 --- /dev/null +++ b/examples/first-render/README.md @@ -0,0 +1,51 @@ +# First render + +Render your first video with the Shotstack API: submit an Edit, poll the render status, and get the +output URL. The same flow is implemented twice, in Node.js and in Python. + +Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). + +## Requirements + +- A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key + (dashboard menu under your account name, top right, under **API Keys**) +- Node.js 18 or later, or Python 3 with [requests](https://pypi.org/project/requests/) + +Sandbox renders are watermarked and don't consume credits, but your account needs at least one +credit to use the environment. + +## Setup + +```bash +git clone https://github.com/shotstack/shotstack-cookbook.git +cd shotstack-cookbook/examples/first-render +``` + +Set your sandbox key: + +```bash +export SHOTSTACK_API_KEY="your_sandbox_api_key" +``` + +Or copy `.env.example` to `.env` and use Node's built-in env-file support (Node run only). + +## Run + +Node.js: + +```bash +node render.mjs # with SHOTSTACK_API_KEY exported +node --env-file=.env render.mjs +``` + +Python: + +```bash +python3 -m pip install requests +python3 render.py +``` + +Both scripts read `edit.json` (a five-second "Hello World" rich-text video), submit it to the +sandbox render endpoint, poll every five seconds until the render reaches `done` or `failed`, and +print the temporary output URL. The URL expires after 24 hours; see the guide for retrieving the +CDN-hosted copy through the Serve API. diff --git a/examples/first-render/edit.json b/examples/first-render/edit.json new file mode 100644 index 0000000..5a57652 --- /dev/null +++ b/examples/first-render/edit.json @@ -0,0 +1,31 @@ +{ + "timeline": { + "background": "#101827", + "tracks": [ + { + "clips": [ + { + "asset": { + "type": "rich-text", + "text": "Hello World", + "font": { + "size": 64, + "color": "#ffffff" + }, + "align": { + "horizontal": "center", + "vertical": "middle" + } + }, + "start": 0, + "length": 5 + } + ] + } + ] + }, + "output": { + "format": "mp4", + "resolution": "preview" + } +} diff --git a/examples/first-render/render.mjs b/examples/first-render/render.mjs new file mode 100644 index 0000000..62e8bd5 --- /dev/null +++ b/examples/first-render/render.mjs @@ -0,0 +1,107 @@ +import { readFile } from 'node:fs/promises'; +import { setTimeout as delay } from 'node:timers/promises'; + +const API_BASE_URL = 'https://api.shotstack.io/edit/stage'; +const POLL_INTERVAL_MS = 5_000; +const MAX_WAIT_MS = 10 * 60 * 1_000; +const apiKey = process.env.SHOTSTACK_API_KEY; + +if (!apiKey) { + throw new Error('Set the SHOTSTACK_API_KEY environment variable first.'); +} + +async function shotstackRequest(path, options = {}) { + const response = await fetch(`${API_BASE_URL}${path}`, { + ...options, + signal: options.signal ?? AbortSignal.timeout(30_000), + headers: { + Accept: 'application/json', + 'x-api-key': apiKey, + ...options.headers, + }, + }); + + const responseText = await response.text(); + let body = null; + + try { + body = JSON.parse(responseText); + } catch { + // The error below includes the raw body when Shotstack does not return JSON. + } + + if (!response.ok) { + const details = body ? JSON.stringify(body) : responseText; + throw new Error( + `Shotstack returned ${response.status} ${response.statusText}: ${details}`, + ); + } + + if (!body) { + throw new Error('Shotstack returned an unexpected non-JSON response.'); + } + + return body; +} + +async function submitRender(edit) { + const result = await shotstackRequest('/render', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(edit), + }); + + const renderId = result?.response?.id; + + if (!renderId) { + throw new Error( + `The response did not contain a render ID: ${JSON.stringify(result)}`, + ); + } + + return renderId; +} + +async function waitForRender(renderId) { + const startedAt = Date.now(); + + while (Date.now() - startedAt < MAX_WAIT_MS) { + const result = await shotstackRequest(`/render/${renderId}`); + const render = result?.response; + + if (!render?.status) { + throw new Error(`Unexpected status response: ${JSON.stringify(result)}`); + } + + console.log(`Render status: ${render.status}`); + + if (render.status === 'done') { + return render; + } + + if (render.status === 'failed') { + throw new Error( + render.error || 'The render failed without an error message.', + ); + } + + await delay(POLL_INTERVAL_MS); + } + + throw new Error(`Render ${renderId} did not finish within 10 minutes.`); +} + +try { + const edit = JSON.parse( + await readFile(new URL('./edit.json', import.meta.url), 'utf8'), + ); + const renderId = await submitRender(edit); + + console.log(`Queued render: ${renderId}`); + + const render = await waitForRender(renderId); + console.log(`Temporary output URL: ${render.url}`); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +} diff --git a/examples/first-render/render.py b/examples/first-render/render.py new file mode 100644 index 0000000..4c76d4d --- /dev/null +++ b/examples/first-render/render.py @@ -0,0 +1,100 @@ +import json +import os +import sys +import time +from pathlib import Path + +import requests + +API_BASE_URL = "https://api.shotstack.io/edit/stage" +POLL_INTERVAL_SECONDS = 5 +MAX_WAIT_SECONDS = 10 * 60 + + +def shotstack_request(method, path, api_key, **kwargs): + response = requests.request( + method, + f"{API_BASE_URL}{path}", + headers={ + "Accept": "application/json", + "x-api-key": api_key, + }, + timeout=30, + **kwargs, + ) + + try: + body = response.json() + except requests.exceptions.JSONDecodeError as error: + raise RuntimeError( + f"Shotstack returned a non-JSON response with status {response.status_code}." + ) from error + + if not response.ok: + raise RuntimeError( + f"Shotstack returned {response.status_code}: {json.dumps(body)}" + ) + + return body + + +def submit_render(edit, api_key): + result = shotstack_request("POST", "/render", api_key, json=edit) + render_id = result.get("response", {}).get("id") + + if not render_id: + raise RuntimeError( + f"The response did not contain a render ID: {json.dumps(result)}" + ) + + return render_id + + +def wait_for_render(render_id, api_key): + started_at = time.monotonic() + + while time.monotonic() - started_at < MAX_WAIT_SECONDS: + result = shotstack_request("GET", f"/render/{render_id}", api_key) + render = result.get("response", {}) + status = render.get("status") + + if not status: + raise RuntimeError(f"Unexpected status response: {json.dumps(result)}") + + print(f"Render status: {status}") + + if status == "done": + return render + + if status == "failed": + raise RuntimeError( + render.get("error") or "The render failed without an error message." + ) + + time.sleep(POLL_INTERVAL_SECONDS) + + raise TimeoutError(f"Render {render_id} did not finish within 10 minutes.") + + +def main(): + api_key = os.environ.get("SHOTSTACK_API_KEY") + + if not api_key: + raise RuntimeError("Set the SHOTSTACK_API_KEY environment variable first.") + + edit_path = Path(__file__).with_name("edit.json") + edit = json.loads(edit_path.read_text(encoding="utf-8")) + render_id = submit_render(edit, api_key) + + print(f"Queued render: {render_id}") + + render = wait_for_render(render_id, api_key) + print(f"Temporary output URL: {render['url']}") + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, RuntimeError, TimeoutError, requests.RequestException) as error: + print(error, file=sys.stderr) + raise SystemExit(1) from error From 2e1b84654d6031b2cd2d85f6f06930b8ef5ddbe1 Mon Sep 17 00:00:00 2001 From: Musab Date: Wed, 5 Aug 2026 04:53:41 +0500 Subject: [PATCH 2/4] docs(first-render): frame the example as the cookbook basics --- README.md | 2 +- examples/first-render/README.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6fb04b3..0fe5ff3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Clone this repo, or head into the folder for the example you want. Each example ## Examples -- [first-render](examples/first-render) submits an Edit, polls the render status, and prints the output URL, in Node.js and Python. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). +- [first-render](examples/first-render) the very basics: submit an Edit, poll the render status, and print the output URL, in Node.js and Python. Start here if you are new to the API. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). - [instagram-ai-video](examples/instagram-ai-video) generates a script, voiceover and background image with AI, renders a 1080x1920 video, and publishes it as an Instagram Reel. Companion code for [How to automate Instagram posts with AI video](https://shotstack.io/learn/automate-instagram-posts-with-ai-video/). - [rapidreels](examples/rapidreels) creates faceless short-form videos using generative AI. [View demo](https://shotstack.io/demos/social-media-video-maker/). - [reelestate](examples/reelestate) turns static real estate images into fully edited video slideshows. [View demo](https://shotstack.io/demos/real-estate-video-listing-maker/). diff --git a/examples/first-render/README.md b/examples/first-render/README.md index 3065444..fc0b4c3 100644 --- a/examples/first-render/README.md +++ b/examples/first-render/README.md @@ -1,7 +1,8 @@ # First render -Render your first video with the Shotstack API: submit an Edit, poll the render status, and get the -output URL. The same flow is implemented twice, in Node.js and in Python. +The very basics of the Shotstack API — the loop every other example in this cookbook builds on: +submit an Edit, poll the render status, and get the output URL. The same flow is implemented twice, +in Node.js and in Python. Start here if you have never rendered a video with Shotstack before. Companion code for [Render your first video with the Shotstack API](https://shotstack.io/learn/render-your-first-video-shotstack-api/). From 44ffe79f39c6c2b28485721689c6677400114d35 Mon Sep 17 00:00:00 2001 From: Musab Date: Wed, 5 Aug 2026 07:06:08 +0500 Subject: [PATCH 3/4] docs(first-render): tidy README opening line --- examples/first-render/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/first-render/README.md b/examples/first-render/README.md index fc0b4c3..ee37df6 100644 --- a/examples/first-render/README.md +++ b/examples/first-render/README.md @@ -1,6 +1,6 @@ # First render -The very basics of the Shotstack API — the loop every other example in this cookbook builds on: +The very basics of the Shotstack API, and the loop every other example in this cookbook builds on: submit an Edit, poll the render status, and get the output URL. The same flow is implemented twice, in Node.js and in Python. Start here if you have never rendered a video with Shotstack before. From 0de6469856df4186c758b22434e3a63244c9332f Mon Sep 17 00:00:00 2001 From: Musab Date: Fri, 7 Aug 2026 06:05:33 +0500 Subject: [PATCH 4/4] Apply cookbook standards: one-line failure output, README format, lint fixes --- examples/first-render/README.md | 13 ++++++------- examples/first-render/render.mjs | 19 ++++++++++--------- examples/first-render/render.py | 12 ++++++++++-- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/examples/first-render/README.md b/examples/first-render/README.md index ee37df6..6609e71 100644 --- a/examples/first-render/README.md +++ b/examples/first-render/README.md @@ -10,7 +10,7 @@ Companion code for [Render your first video with the Shotstack API](https://shot - A [Shotstack account](https://dashboard.shotstack.io/register) and your **sandbox** API key (dashboard menu under your account name, top right, under **API Keys**) -- Node.js 18 or later, or Python 3 with [requests](https://pypi.org/project/requests/) +- Node.js 20 or later, or Python 3 with [requests](https://pypi.org/project/requests/) Sandbox renders are watermarked and don't consume credits, but your account needs at least one credit to use the environment. @@ -28,15 +28,12 @@ Set your sandbox key: export SHOTSTACK_API_KEY="your_sandbox_api_key" ``` -Or copy `.env.example` to `.env` and use Node's built-in env-file support (Node run only). - ## Run Node.js: ```bash -node render.mjs # with SHOTSTACK_API_KEY exported -node --env-file=.env render.mjs +node render.mjs ``` Python: @@ -46,7 +43,9 @@ python3 -m pip install requests python3 render.py ``` +## What happens + Both scripts read `edit.json` (a five-second "Hello World" rich-text video), submit it to the sandbox render endpoint, poll every five seconds until the render reaches `done` or `failed`, and -print the temporary output URL. The URL expires after 24 hours; see the guide for retrieving the -CDN-hosted copy through the Serve API. +print the temporary output URL. A sandbox render finishes in under a minute. The URL expires after +24 hours; see the guide for retrieving the CDN-hosted copy through the Serve API. diff --git a/examples/first-render/render.mjs b/examples/first-render/render.mjs index 62e8bd5..178d232 100644 --- a/examples/first-render/render.mjs +++ b/examples/first-render/render.mjs @@ -7,18 +7,19 @@ const MAX_WAIT_MS = 10 * 60 * 1_000; const apiKey = process.env.SHOTSTACK_API_KEY; if (!apiKey) { - throw new Error('Set the SHOTSTACK_API_KEY environment variable first.'); + console.error('Set the SHOTSTACK_API_KEY environment variable first.'); + process.exit(1); } async function shotstackRequest(path, options = {}) { const response = await fetch(`${API_BASE_URL}${path}`, { ...options, - signal: options.signal ?? AbortSignal.timeout(30_000), + signal: AbortSignal.timeout(30_000), headers: { Accept: 'application/json', 'x-api-key': apiKey, - ...options.headers, - }, + ...options.headers + } }); const responseText = await response.text(); @@ -33,7 +34,7 @@ async function shotstackRequest(path, options = {}) { if (!response.ok) { const details = body ? JSON.stringify(body) : responseText; throw new Error( - `Shotstack returned ${response.status} ${response.statusText}: ${details}`, + `Shotstack returned ${response.status} ${response.statusText}: ${details}` ); } @@ -48,14 +49,14 @@ async function submitRender(edit) { const result = await shotstackRequest('/render', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(edit), + body: JSON.stringify(edit) }); const renderId = result?.response?.id; if (!renderId) { throw new Error( - `The response did not contain a render ID: ${JSON.stringify(result)}`, + `The response did not contain a render ID: ${JSON.stringify(result)}` ); } @@ -81,7 +82,7 @@ async function waitForRender(renderId) { if (render.status === 'failed') { throw new Error( - render.error || 'The render failed without an error message.', + render.error || 'The render failed without an error message.' ); } @@ -93,7 +94,7 @@ async function waitForRender(renderId) { try { const edit = JSON.parse( - await readFile(new URL('./edit.json', import.meta.url), 'utf8'), + await readFile(new URL('./edit.json', import.meta.url), 'utf8') ); const renderId = await submitRender(edit); diff --git a/examples/first-render/render.py b/examples/first-render/render.py index 4c76d4d..c7cd51d 100644 --- a/examples/first-render/render.py +++ b/examples/first-render/render.py @@ -27,7 +27,8 @@ def shotstack_request(method, path, api_key, **kwargs): body = response.json() except requests.exceptions.JSONDecodeError as error: raise RuntimeError( - f"Shotstack returned a non-JSON response with status {response.status_code}." + "Shotstack returned a non-JSON response " + f"with status {response.status_code}." ) from error if not response.ok: @@ -95,6 +96,13 @@ def main(): if __name__ == "__main__": try: main() - except (OSError, ValueError, RuntimeError, TimeoutError, requests.RequestException) as error: + except ( + OSError, + KeyError, + ValueError, + RuntimeError, + TimeoutError, + requests.RequestException, + ) as error: print(error, file=sys.stderr) raise SystemExit(1) from error