← OrshotCONTENT HISTORY

Update to Orshot

Snapshot Oct 24, 2026 · 12:51 UTC · version 3.3.0

Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.

WHAT CHANGED

Payment or plan references changed

Full description — Now says: another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by...

Observed in published text. Live prices and checkout terms have not been verified by this change.

Save Orshot and check future changes in Following.

Up to 3 plugins free · no card required.

All fields and original values.

Full description

Before

Sep 30, 2026 · 22:48 UTC

Bannerbear, Placid, Creatomate, RenderForm, Abyssale, or similar. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, video output nee...

After

Oct 24, 2026 · 12:51 UTC

another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render ...

Skill instructions

Before

Sep 30, 2026 · 22:48 UTC

Bannerbear, Placid, Creatomate, RenderForm, Abyssale, or similar. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, video output nee...

After

Oct 24, 2026 · 12:51 UTC

another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render ...

Compare saved observations

Download comparison JSON

SKILL.md text changes

− removed · + added

--- before
+++ after
@@ -1,9 +1,9 @@
 ---
 name: orshot
-description: Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from Bannerbear, Placid, Creatomate, RenderForm, Abyssale, or similar. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, video output needs video elements in the template) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.
+description: Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render jobs) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.
 metadata:
   author: Rishi Mohan
-  version: "1.1.0"
+  version: "1.3.0"
 ---
 
 # Orshot – Automated Visual Content Generation
@@ -25,7 +25,7 @@
 - Automating visuals with Zapier, Make, n8n, Airtable, or the CLI
 - Connecting Orshot to an AI agent or MCP client (Claude, Cursor, Codex, Windsurf, ChatGPT)
 - Embedding a white-label design editor into an app
-- Migrating from Bannerbear, Placid, Creatomate, RenderForm, Abyssale, DynaPictures, or Contentdrips
+- Migrating from another template-based image/PDF/video generation API
 
 **Don't use this skill** for generating standalone AI images with no template or Orshot involved (a one-off "make me an image" request) — that isn't what Orshot does.
 
@@ -40,7 +40,6 @@
 https://orshot.com/docs/publish/publish-from-api.md
 https://orshot.com/docs/developers/oauth-overview.md
 https://orshot.com/docs/orshot-embed/introduction.md
-https://orshot.com/blog/bannerbear-api-alternative.md
 ```
 
 **Key documentation pages:**
@@ -150,7 +149,7 @@
 1. **Modifications key by `parameterId`, not layer name.** `modifications: { "headline": "..." }` targets the element whose `parameterId` is `headline`. If a value is ignored, the key is wrong — fetch the template's modifications (`GET /v1/studio/templates/:id`) to see the real keys instead of guessing.
 2. **Multi-page templates need a `pageN@` prefix.** Use `"page1@title"`, `"page2@title"`. A bare `"title"` only affects page 1.
 3. **Studio `templateId` is an integer; utility `templateId` is a string.** `/v1/studio/render` takes an integer ID; `/v1/generate/:renderType` takes a string slug like `"website-screenshot"`.
-4. **Video output requires video elements in the template.** Requesting `format: "mp4"` on an image-only template fails — the template must contain at least one video element.
+4. **Video output does NOT require video elements.** `format: "mp4"`/`"webm"`/`"gif"` renders any template — motion can come from video elements, page animations (enter/exit/ambient transitions), page-to-page transitions, or generated narration; a fully static page renders as a still clip of its `videoDuration`. Long video renders should use async render jobs (`response.mode: "async"`) so they don't hit client timeouts.
 5. **Image/video URLs in modifications must be publicly reachable.** The renderer fetches them server-side, so `localhost`, expired signed URLs, or auth-gated URLs won't load.
 6. **Style overrides use dot notation on the parameterId** — `"title.fontSize": "48px"`, not a nested object.
 7. **Response shape differs by page count:** single page → `data` is an object (read `data.content`); multi-page/carousel → `data` is an array of `{ page, content }`. Handle both.
@@ -956,6 +955,79 @@
 });
 ```
 
+### Render-level `videoOptions` (top level, sibling of `modifications`)
+
+Controls the whole video render, not one element:
+
+```json
+{
+  "templateId": 123,
+  "modifications": { "page1@headline": "Launch day" },
+  "videoOptions": {
+    "duration": 12,
+    "fps": 30,
+    "muted": false,
+    "combinePages": true,
+    "pageTransition": "fade",
+    "pageTransitionDuration": 0.5,
+    "audioSource": "https://example.com/track.mp3",
+    "subtitleSource": "https://example.com/voiceover.mp3",
+    "captionPreset": "karaoke"
+  },
+  "response": { "type": "url", "format": "mp4" }
+}
+```
+
+- `duration`, `fps` (≤30), `quality`, `muted`, `trimStart`+`trimEnd` (must be paired).
+- `combinePages: true` merges a multi-page template into ONE video, with `pageTransition` (42 types — fades, slides, wipes, zooms, blurs and more) and `pageTransitionDuration` (0–2s, default 0.5) between scenes. A string `audioSource` then lays one continuous soundtrack across the combined video — the recipe for multi-scene videos.
+- `audioSource` swaps an existing URL audio track at render time while keeping its saved trim/fades/volume/offset/loop. Generated narration changes through `tts.parameterId` instead (see Audio below).
+- Captions: `subtitleSource` (a media URL to auto-transcribe, or a `.srt`/`.vtt` used as-is), `captionPreset` (`pill` | `minimal` | `karaoke` | `bold` | `outline` | `neon`), plus fine-grained `subtitleMode` (`phrase`|`word-by-word`), `subtitleWordReveal` (`appear`|`highlight`|`align`), `subtitleSplit` (`word`|`letter`), `subtitleMaxWordsPerLine` or `subtitleMaxCharsPerLine`, and `subtitle*` styling keys. Prefer authored `page.subtitle` with source `"auto-audio"` so captions word-sync automatically.
+
+### Async render jobs (long videos — use this from automations)
+
+A synchronous render holds the HTTP connection for the full render time; video routinely outlives client timeouts (Zapier ~30s, many MCP clients 120s). Add `response.mode: "async"`:
+
+```
+POST /v1/studio/render  { ..., "response": { "mode": "async", "format": "mp4" } }
+  → 202 with a job object in ~100ms
+GET  /v1/studio/render-jobs/:id          → poll the job (queued → processing → succeeded/failed/canceled)
+GET  /v1/studio/render-jobs?status=&limit=  → newest-first list
+POST /v1/studio/render-jobs/:id/cancel   → cancel while not terminal
+```
+
+The job's `result` is byte-identical to the sync response. Pass a `webhook_url` to get the settled job POSTed to you instead of polling. Jobs time out at 15 minutes.
+
+### Page animations & ambient motion (video templates)
+
+Elements animate via a page-level `transitions` object on the element — no video file needed:
+
+```json
+{
+  "transitions": {
+    "enter": { "type": "slideInUp", "duration": 0.5, "easing": "ease-out" },
+    "showAt": 0.4,
+    "ambient": { "type": "kenBurns", "customValues": { "zoom": 1.12, "panX": 6 } },
+    "exit": { "type": "fadeOut", "duration": 0.3 },
+    "hideAt": 3.6
+  }
+}
+```
+
+- Three phases: `enter` → `ambient` (plays the element's whole visible life) → `exit`. `showAt`/`hideAt` are siblings of `enter`/`exit`, never nested inside them (the #1 authoring bug).
+- Ambient types: `kenBurns` (runs once, stretched across visibility — give every full-bleed photo one), plus looping `pulse`, `float`, `drift`, `wiggle`, `shake`, `slowRotate`, `breathe`. `shake`/`slowRotate` need `easing: "linear"` or the loop seam shows.
+- All timing is tunable per render via dot-notation modifications: `"photo.showAt": 2.1`, `"photo.enterType": "scaleIn"`, `"photo.ambientType": "kenBurns"`, etc.
+
+### Audio: soundtracks & AI voiceover (video templates)
+
+Each page carries `audioTracks` (max 10, page-level, mixed into every mp4/webm render):
+
+- **Existing audio**: `{ "url": "https://...", "trimStart": 33.2, "trimEnd": 44.1, "fadeOut": 1, "volume": 1, "loop": false, "offset": 0 }` — trim to the song's best part; `videoOptions.audioSource` swaps the file at render keeping these settings.
+- **AI voiceover**: `{ "tts": { "text": "words to speak", "voiceId": "...", "modelId": "flash", "parameterId": "voiceover" } }` instead of `url`. Synthesized when the template is saved (metered voice minutes; identical scripts are cached and free on re-render). Narration length drives the page length, and `page.subtitle` with source `"auto-audio"` gives word-synced captions with no transcription step.
+- **Generated ambient/SFX**: `{ "sfx": { "prompt": "warm modern pulse, instrumental, seamless loop", "seconds": 12 }, "volume": 0.12, "loop": true }`.
+- `tts.parameterId` makes the SPOKEN TEXT a render parameter: `modifications: { "voiceover": "new words" }` (multi-page: `"page1@voiceover"`) re-voices the video per render; `"voiceover.voiceId"` / `"voiceover.modelId"` tune the voice.
+- Budget narration at ~2 words/second plus 1 second of headroom for the page duration.
+- Via MCP, call `orshot_get_audio_authoring_options` first — it returns valid voice ids, models and the workspace's voice allowance in one read-only call.
+
 ### Smart Resize (one design → any size)
 
 Render a template at a **different canvas size without redesigning it** — the layout is deterministically re-solved to fit (elements re-anchor, backgrounds stretch, groups move together). Set these on the `response` object:
@@ -1001,7 +1073,7 @@
 | `mkv`  | Video | Matroska container                         |
 | `gif`  | Video | Animated, no audio support                 |
 
-### Render Usage & Costs
+### Render Usage (credits)
 
 Usage is measured in **credits**. 1 credit = 1 image, 1 PDF page, or 1 second of video.
 
@@ -1079,7 +1151,7 @@
 
 ### Choose the road first
 
-Check what they already run before pitching anything. `GET /v1/workspace/logs?limit=100`
+Check what they already run before recommending anything. `GET /v1/workspace/logs?limit=100`
 returns a `source` on every row, which answers it factually:
 
 | `source` in their logs | They already run | Lead with |
@@ -1091,7 +1163,7 @@
 | `orshot-*-sdk`, `api`, `cli` | Their own code | Write the call into their repo |
 | only `playground` / `orshot-mcp-server` | Nothing yet | Offer Orshot Workflows |
 
-Never pitch a second orchestrator to someone who already has one. A user with a
+Never recommend a second orchestrator to someone who already has one. A user with a
 live n8n setup wants this template added to it, not a new tool to learn.
 
 If you have browser control or repo access, offer to DO the setup rather than
@@ -1111,9 +1183,9 @@
 4. `orshot_get_workflow_connection_data` — resolve "my content sheet" into a
    concrete id, and confirm the name back to the user
 5. `orshot_validate_workflow` — read `errors`, `warnings` AND `gated`. A `gated`
-   entry means a plan block, not a config problem: the draft still saves, only
-   activation is blocked, so tell the user which plan unlocks it instead of
-   editing steps
+   entry means the step is not included in the workspace's current plan, not a
+   config problem: the draft still saves, only activation is blocked, so tell
+   the user that instead of editing steps
 6. `orshot_create_workflow` with `status: "draft"`, then share the edit link from
    the result — it opens pre-configured in their dashboard
 7. `orshot_run_workflow` — prove it works, then they activate
@@ -1192,9 +1264,9 @@
 | 400  | `templateId missing`           | Add `templateId` to request body             |
 | 400  | `Invalid API Key`              | Generate new key from dashboard              |
 | 403  | `Authorization header missing` | Add `Authorization: Bearer <KEY>` header     |
-| 403  | `Subscription inactive`        | Check usage or upgrade plan                  |
+| 403  | `Subscription inactive`        | The workspace subscription is not active; check it in the dashboard |
 | 403  | `Template not found`           | Verify template ID belongs to your workspace |
-| 403  | `Video on free plan`           | Upgrade to paid plan for video generation    |
+| 403  | `ai-video-not-on-plan`         | AI video is not included in the workspace's current plan; use stock footage or a Brand Library video instead |
 
 ### General Best Practices
 
@@ -1267,30 +1339,34 @@
 
 ## Developer Apps & OAuth
 
-Build third-party integrations with Orshot using OAuth 2.0. Register apps, authenticate users, and access their workspaces programmatically.
+Build third-party integrations with Orshot using OAuth 2.0. Register an app, authenticate users, and access their workspaces on their behalf.
 
 For detailed setup: fetch `https://orshot.com/docs/developers/oauth-overview.md`
 
 ### Register an App
 
-1. Go to **Workspace Settings → Developer Apps** in Orshot dashboard
-2. Create a new app with name, redirect URI, and required scopes
-3. Receive `client_id` and `client_secret`
+1. Go to **Workspace Settings → Developer Apps** in the Orshot dashboard
+2. Create a new app with a name, redirect URI, and the scopes it needs
+3. Use the app's `client_id` in the flows below. Orshot OAuth clients are public clients: PKCE (`S256`) is required and no client secret is used.
+
+Discovery: `https://api.orshot.com/.well-known/oauth-authorization-server` (dynamic client registration is available at `registration_endpoint`).
 
 ### OAuth 2.0 Flows
 
-**Authorization Code Flow** (web apps):
+**Authorization Code + PKCE** (web and desktop apps):
 ```
 GET https://orshot.com/oauth/authorize?
   response_type=code&
   client_id=YOUR_CLIENT_ID&
   redirect_uri=https://yourapp.com/callback&
+  code_challenge=BASE64URL_SHA256_OF_VERIFIER&
+  code_challenge_method=S256&
   scope=workspace:templates:read workspace:templates:write render:generate
 ```
 
 **Device Flow** (CLI/headless):
 ```
-POST https://orshot.com/oauth/device/code
+POST https://api.orshot.com/v1/oauth/device/code
   client_id=YOUR_CLIENT_ID&
   scope=workspace:templates:read render:generate
 ```
@@ -1298,14 +1374,16 @@
 ### Token Exchange
 
 ```
-POST https://orshot.com/oauth/token
+POST https://api.orshot.com/v1/oauth/token
   grant_type=authorization_code&
   code=AUTH_CODE&
   client_id=YOUR_CLIENT_ID&
-  client_secret=YOUR_CLIENT_SECRET&
+  code_verifier=THE_PKCE_VERIFIER&
   redirect_uri=https://yourapp.com/callback
 ```
 
+Refresh with `grant_type=refresh_token` when the `offline_access` scope was granted. Store tokens server-side or in the user's own environment; never place them in prompts, logs, or shared files.
+
 ### Available Scopes
 
 | Scope | Description |
@@ -1383,344 +1461,18 @@
 For Vue SDK details: fetch `https://orshot.com/docs/orshot-embed/vue-sdk.md`
 For webhook setup: fetch `https://orshot.com/docs/orshot-embed/webhooks.md`
 
-## Migrating from Other Platforms
-
-This section maps authentication, endpoints, and request formats for migrating from competing platforms. For detailed comparison articles, fetch the corresponding blog post URL.
+## Migrating from Another Template-Render API
 
-### Migration Playbook (any platform)
-
-The API-call swap is the easy part (mapped per-platform below). The real work is recreating the design, because you can't import another platform's template JSON:
+The API-call swap is the easy part. The real work is recreating the design, because another platform's template JSON cannot be imported:
 
 1. **Recreate the template in Orshot Studio** (or via `POST /v1/studio/templates/create`). Rebuild the layout, then mark every dynamic layer parameterizable with a clear `parameterId`.
-2. **Map old field names → Orshot `parameterId`s.** Other platforms key by layer name, `image_url`, `payload`, etc.; Orshot keys by `parameterId`. Keep a lookup from old names to new ones.
-3. **Swap the API call** using the tables below — almost always `Authorization: Bearer` + `POST /v1/studio/render`.
-4. **Handle sync vs async.** Most platforms are async (submit → poll/webhook); Orshot renders **synchronously**, so delete the polling/webhook code and read `data.content` from the response.
-5. **Verify the response shape** — single page is an object, multi-page is an array (Common Gotchas #7) — before cutting traffic over.
-
-### Migrating from BannerBear
-
-Blog: `https://orshot.com/blog/bannerbear-api-alternative.md`
-
-**Authentication:**
-| BannerBear | Orshot |
-|------------|--------|
-| `Authorization: Bearer BB_API_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | BannerBear | Orshot |
-|--------|-----------|--------|
-| Generate image | `POST /v2/images` (async, returns 202) | `POST /v1/studio/render` (sync) |
-| Generate image (sync) | `POST sync.api.bannerbear.com/v2/images` (10s timeout) | `POST /v1/studio/render` (sync by default) |
-| List templates | `GET /v2/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |
-| Get template | `GET /v2/templates/:uid` | `GET /v1/studio/templates/:id` |
-| Generate video | `POST /v2/videos` | `POST /v1/studio/render` with `format: "mp4"` |
-| Multi-template batch | `POST /v2/collections` | Loop `POST /v1/studio/render` per template |
-
-**Request Format Translation:**
-
-BannerBear uses a `modifications` **array** with named layers:
-```json
-// BannerBear
-{
-  "template": "TEMPLATE_UID",
-  "modifications": [
-    { "name": "title", "text": "Hello World" },
-    { "name": "hero", "image_url": "https://example.com/img.jpg" },
-    { "name": "bg", "color": "#FF0000" }
-  ]
-}
-
-// Orshot equivalent
-{
-  "templateId": 123,
-  "modifications": {
-    "title": "Hello World",
-    "hero": "https://example.com/img.jpg",
-    "canvasBackgroundColor": "#FF0000"
-  },
-  "response": { "type": "url", "format": "png" }
-}
-```
-
-**Key differences:**
-- BannerBear modifications is an **array**, Orshot is a **flat object**
-- BannerBear uses `image_url` for images, Orshot uses the parameter ID directly with a URL value
-- BannerBear requires polling for async results, Orshot returns synchronously
-- BannerBear `template` is a UID string, Orshot `templateId` is an integer for studio templates
-- Orshot supports style overrides via dot notation (e.g., `"title.fontSize": "48px"`) — BannerBear does not
-
-### Migrating from Placid
-
-Blog: `https://orshot.com/blog/placid-api-alternative.md`
-
-**Authentication:**
-| Placid | Orshot |
-|--------|--------|
-| `Authorization: Bearer PLACID_TOKEN` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | Placid | Orshot |
-|--------|--------|--------|
-| Generate image | `POST /api/rest/{template_uuid}` | `POST /v1/studio/render` |
-| Get image status | `GET /api/rest/images/{id}` | Not needed (sync response) |
-| List templates | `GET /api/rest/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |
-| Get template | `GET /api/rest/templates/{uuid}` | `GET /v1/studio/templates/:id` |
-| Delete render | `DELETE /api/rest/images/{id}` | Not needed (renders don't expire) |
-
-**Request Format Translation:**
-
-Placid uses a `layers` **object** with type-specific properties:
-```json
-// Placid
-{
-  "layers": {
-    "title": {
-      "text": "Hello World",
-      "text_color": "#FF0000",
-      "font": "Arial"
-    },
-    "hero_image": {
-      "image": "https://example.com/img.jpg"
-    },
-    "background": {
-      "background_color": "#000000"
-    }
-  },
-  "modifications": {
-    "width": 1200,
-    "height": 630,
-    "image_format": "jpg"
-  }
-}
-
-// Orshot equivalent
-{
-  "templateId": 123,
-  "modifications": {
-    "title": "Hello World",
-    "title.color": "#FF0000",
-    "title.fontFamily": "Arial",
-    "hero_image": "https://example.com/img.jpg",
-    "canvasBackgroundColor": "#000000"
-  },
-  "response": { "type": "url", "format": "jpg" }
-}
-```
-
-**Key differences:**
-- Placid separates `layers` (content) from `modifications` (output settings), Orshot combines both in `modifications`
-- Placid uses `text_color`, Orshot uses dot notation `parameterId.color`
-- Placid uses `image` key for image URLs, Orshot uses the parameter ID directly
-- Placid requires polling or webhooks, Orshot returns synchronously
-- Placid renders can expire, Orshot renders persist
-- Placid credits vary by resolution, Orshot is a flat 1 credit per image regardless of size
-
-### Migrating from Creatomate
-
-Blog: `https://orshot.com/blog/creatomate-api-alternative.md`
-
-**Authentication:**
-| Creatomate | Orshot |
-|------------|--------|
-| `Authorization: Bearer CREATOMATE_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | Creatomate | Orshot |
-|--------|-----------|--------|
-| Generate render | `POST /v1/renders` | `POST /v1/studio/render` |
-| Get render status | `GET /v1/renders/:id` | Not needed (sync response) |
-| List templates | `GET /v1/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |
-| Get template | `GET /v1/templates/:id` | `GET /v1/studio/templates/:id` |
-
-**Request Format Translation:**
-
-Creatomate uses a flat `modifications` object (closest to Orshot's format):
-```json
-// Creatomate
-{
-  "template_id": "TEMPLATE_UUID",
-  "modifications": {
-    "Title": "Hello World",
-    "Image-1": "https://example.com/img.jpg"
-  },
-  "output_format": "jpg",
-  "render_scale": 1,
-  "max_width": 1080
-}
-
-// Orshot equivalent
-{
-  "templateId": 123,
-  "modifications": {
-    "title": "Hello World",
-    "image_1": "https://example.com/img.jpg"
-  },
-  "response": { "type": "url", "format": "jpg", "scale": 1 }
-}
-```
-
-**Key differences:**
-- Creatomate `template_id` is a UUID string, Orshot `templateId` is an integer
-- Creatomate element names are display names (e.g., "Title", "Image-1"), Orshot uses snake_case parameterIds
-- Creatomate has `output_format` at root level, Orshot uses `response.format`
-- Creatomate requires polling/webhooks, Orshot returns synchronously
-- Creatomate renders can expire, Orshot renders persist
-- Orshot supports style overrides via dot notation — Creatomate requires modifying the template source JSON
-- Creatomate gates its preview SDK to a higher tier; Orshot's embed is available on all paid plans
-
-### Migrating from RenderForm
-
-Blog: `https://orshot.com/blog/renderform-api-alternative.md`
-
-**Authentication:**
-| RenderForm | Orshot |
-|------------|--------|
-| `X-API-KEY: RENDERFORM_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | RenderForm | Orshot |
-|--------|-----------|--------|
-| Generate image | `POST /api/v2/render` | `POST /v1/studio/render` |
-| List templates | `GET /api/v2/my-templates?page=1&size=50` | `GET /v1/studio/templates/all?page=1&limit=10` |
-| Get template | `GET /api/v2/my-templates/:id` | `GET /v1/studio/templates/:id` |
-| URL-based render | `GET /img/TEMPLATE.jpg?key=...&param=...` | `GET /v1/studio/dynamic-url/TEMPLATE?param=...` |
-
-**Request Format Translation:**
-
-RenderForm uses dot notation in a `data` object (similar to Orshot's style overrides):
-```json
-// RenderForm
-{
-  "template": "TEMPLATE_ID",
-  "data": {
-    "title.text": "Hello World",
-    "hero.src": "https://example.com/img.jpg",
-    "bg.color": "#FF0000"
-  },
-  "fileName": "output",
-  "width": 1200,
-  "height": 630
-}
-
-// Orshot equivalent
-{
-  "templateId": 123,
-  "modifications": {
-    "title": "Hello World",
-    "hero": "https://example.com/img.jpg",
-    "canvasBackgroundColor": "#FF0000"
-  },
-  "response": { "type": "url", "format": "png", "fileName": "output" }
-}
-```
-
-**Key differences:**
-- RenderForm uses `X-API-KEY` header, Orshot uses `Authorization: Bearer`
-- RenderForm uses `data` with `componentId.property` dot notation for everything, Orshot uses `modifications` with parameter IDs for content and dot notation only for style overrides
-- RenderForm `template` is a string ID, Orshot `templateId` is an integer
-- RenderForm images can expire, Orshot renders persist
-- Orshot's free tier has no watermarks
-
-### Migrating from Abyssale
-
-Blog: `https://orshot.com/blog/abyssale-api-alternative.md`
-
-**Authentication:**
-| Abyssale | Orshot |
-|----------|--------|
-| `x-api-key: ABYSSALE_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | Abyssale | Orshot |
-|--------|---------|--------|
-| Generate image | `POST /async/banner-builder/{designId}/generate` (async) | `POST /v1/studio/render` (sync) |
-| Poll status | `GET /generation-request/{requestId}` | Not needed (sync response) |
-| List designs | `GET /designs` | `GET /v1/studio/templates/all?page=1&limit=10` |
-| Get design | `GET /designs/{designId}` | `GET /v1/studio/templates/:id` |
-| Multi-format render | `template_format_names` param | Loop `POST /v1/studio/render` per format, or use Variant Generation API |
-
-**Request Format Translation:**
-
-Abyssale uses an `elements` object with `payload` for text:
-```json
-// Abyssale
-{
-  "template_format_names": ["facebook-feed", "instagram-post"],
-  "elements": {
-    "title": { "payload": "Hello World" },
-    "hero_image": { "image_url": "https://example.com/img.jpg" },
-    "background": { "color": "#FF0000" }
-  },
-  "callback_url": "https://webhook.example.com"
-}
-
-// Orshot equivalent
-{
-  "templateId": 123,
-  "modifications": {
-    "title": "Hello World",
-    "hero_image": "https://example.com/img.jpg",
-    "canvasBackgroundColor": "#FF0000"
-  },
-  "response": { "type": "url", "format": "png" }
-}
-```
+2. **Map old field names → Orshot `parameterId`s.** Other APIs key by layer name or by fixed keys such as `image_url`; Orshot keys by `parameterId`. Keep a lookup from old names to new ones.
+3. **Swap the API call**: `Authorization: Bearer <ORSHOT_API_KEY>` + `POST /v1/studio/render` with `modifications` as a flat object (page-prefixed keys for multi-page templates).
+4. **Handle sync vs async.** Many render APIs are submit-then-poll; Orshot image and PDF renders return synchronously, so delete the polling/webhook code and read `data.content` from the response. Long video renders use async render jobs (see above).
+5. **Style overrides** that other APIs put in per-layer objects become dot-notation keys here (`"title.fontSize": "48px"`).
+6. **Verify the response shape** — single page is an object, multi-page is an array (Common Gotchas #7) — before cutting traffic over.
 
-**Key differences:**
-- Abyssale uses `payload` for text content, Orshot uses the parameter ID directly
-- Abyssale uses `image_url` for images, Orshot uses the parameter ID with URL value
-- Abyssale supports multi-format in single call via `template_format_names`, Orshot requires separate calls or Variant Generation API
-- Abyssale is async-only (polling/webhook), Orshot returns synchronously
-- Abyssale charges per-seat, Orshot has unlimited team members on all plans
-- Abyssale results can expire, Orshot renders persist
-
-### Migrating from DynaPictures
-
-Blog: `https://orshot.com/blog/dynapictures-api-alternative.md`
-
-**Authentication:**
-| DynaPictures | Orshot |
-|--------------|--------|
-| `Authorization: Bearer DYNA_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |
-
-**Endpoint Mapping:**
-| Action | DynaPictures | Orshot |
-|--------|-------------|--------|
-| Generate image | `POST /designs/{id}` | `POST /v1/studio/render` |
-| List designs | `GET /designs` | `GET /v1/studio/templates/all?page=1&limit=10` |
-
-**Key differences:**
-- DynaPictures uses an outdated editor interface, Orshot has a modern Figma-like editor
-- No multi-page support in DynaPictures
-- No white-label editor option
-- Limited integrations compared to Orshot's 15+
-
-### Migrating from Contentdrips
-
-Blog: `https://orshot.com/blog/contentdrips-api-alternative.md`
-
-Contentdrips is a social media design tool with API as a secondary feature. Migration is straightforward since Orshot is API-first.
-
-**Key differences:**
-- Contentdrips API is bolted-on, Orshot is API-first
-- Contentdrips has limited dynamic parameter capabilities
-- No white-label editor, no MCP server, no CLI
-- Per-seat pricing vs. Orshot's credit-based pricing
-
-### Quick Migration Reference
-
-| Feature | BannerBear | Placid | Creatomate | RenderForm | Abyssale | Orshot |
-|---------|-----------|--------|------------|------------|----------|--------|
-| **Auth header** | `Authorization: Bearer` | `Authorization: Bearer` | `Authorization: Bearer` | `X-API-KEY` | `x-api-key` | `Authorization: Bearer` |
-| **Modifications format** | Array of objects | `layers` object | Flat object | `data` with dot notation | `elements` with `payload` | Flat object |
-| **Template ID type** | UID string | UUID string | UUID string | String | UUID string | Integer (studio) |
-| **Response model** | Async (poll) | Async (poll) | Async (poll) | Sync | Async (poll) | Sync |
-| **Render persistence** | Permanent | Can expire | Can expire | Can expire | Can expire | Permanent |
-| **Style overrides** | No | Limited | Via source JSON | Dot notation | No | Dot notation |
-| **Multi-page** | No | No | No | No | No | Yes |
-| **Video support** | Yes (extra cost) | Yes (extra cost) | Yes | No | Animated only | Yes (1 credit/sec) |
-| **White-label editor** | No | No | Higher tier | No | No | All paid plans |
-| **Social publishing** | No | No | No | No | No | 13+ platforms |
+Render URLs on Orshot persist; do not add expiry handling unless you delete renders yourself.
 
 ### Links
 
@@ -1729,7 +1481,6 @@
 - Integrations: https://orshot.com/integrations
 - MCP Server: https://orshot.com/docs/integrations/mcp-server
 - Templates: https://orshot.com/templates
-- Pricing: https://orshot.com/pricing
 - Developer Apps: https://orshot.com/docs/developers
 - Social Publishing: https://orshot.com/docs/publish/introduction
 - Orshot Embed: https://orshot.com/docs/orshot-embed/introduction
Full technical diff · 2 changed fields

changed /description

BEFORE
"Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from Bannerbear, Placid, Creatomate, RenderForm, Abyssale, or similar. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, video output needs video elements in the template) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved."
AFTER
"Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render jobs) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved."

changed /skill_md_contents

BEFORE
"---\nname: orshot\ndescription: Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from Bannerbear, Placid, Creatomate, RenderForm, Abyssale, or similar. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, video output needs video elements in the template) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.\nmetadata:\n  author: Rishi Mohan\n  version: \"1.1.0\"\n---\n\n# Orshot – Automated Visual Content Generation\n\n[Orshot](https://orshot.com) is an automated image, PDF, and video generation platform. Design templates in Orshot Studio (or import from Canva/Figma), then generate renders via REST API, SDKs, or no-code integrations.\n\n- **Documentation:** https://orshot.com/docs\n- **API Base URL:** https://api.orshot.com/v1\n\n## When to Use This Skill\n\nUse this skill whenever the user mentions **Orshot**, or when the task is:\n\n- Generating images, PDFs, or videos programmatically from templates\n- Building automated marketing visual pipelines (OG images, ad creatives, thumbnails)\n- Creating dynamic social media content (carousels, posts, stories) and publishing it\n- Generating certificates, invoices, tickets, or reports as PDFs\n- Building image/PDF/video generation into a product\n- Automating visuals with Zapier, Make, n8n, Airtable, or the CLI\n- Connecting Orshot to an AI agent or MCP client (Claude, Cursor, Codex, Windsurf, ChatGPT)\n- Embedding a white-label design editor into an app\n- Migrating from Bannerbear, Placid, Creatomate, RenderForm, Abyssale, DynaPictures, or Contentdrips\n\n**Don't use this skill** for generating standalone AI images with no template or Orshot involved (a one-off \"make me an image\" request) — that isn't what Orshot does.\n\n## Accessing Detailed Documentation\n\nAny page on orshot.com can be fetched as clean markdown by appending `.md` to the URL or sending `Accept: text/markdown` header. Use this to get detailed, up-to-date information on demand without relying solely on this skill file.\n\n**Examples:**\n```\nhttps://orshot.com/docs/api-reference.md\nhttps://orshot.com/docs/sdks/node.md\nhttps://orshot.com/docs/publish/publish-from-api.md\nhttps://orshot.com/docs/developers/oauth-overview.md\nhttps://orshot.com/docs/orshot-embed/introduction.md\nhttps://orshot.com/blog/bannerbear-api-alternative.md\n```\n\n**Key documentation pages:**\n| Topic | URL |\n|-------|-----|\n| API Reference | `https://orshot.com/docs/api-reference.md` |\n| Node.js SDK | `https://orshot.com/docs/sdks/node.md` |\n| Python SDK | `https://orshot.com/docs/sdks/python.md` |\n| PHP SDK | `https://orshot.com/docs/sdks/php.md` |\n| Ruby SDK | `https://orshot.com/docs/sdks/ruby.md` |\n| Studio Templates | `https://orshot.com/docs/orshot-studio/introduction.md` |\n| Style Parameters | `https://orshot.com/docs/orshot-studio/style-parameters.md` |\n| Setting Parameters | `https://orshot.com/docs/orshot-studio/setting-parameters.md` |\n| Image Generation | `https://orshot.com/docs/image-generation.md` |\n| Video Generation | `https://orshot.com/docs/video-generation.md` |\n| PDF Generation | `https://orshot.com/docs/pdf-generation.md` |\n| Social Publishing | `https://orshot.com/docs/publish/introduction.md` |\n| OAuth / Developer Apps | `https://orshot.com/docs/developers.md` |\n| White-Label Embed | `https://orshot.com/docs/orshot-embed/introduction.md` |\n| Integrations | `https://orshot.com/docs/integrations.md` |\n| Dynamic URLs | `https://orshot.com/docs/integrations/dynamic-urls.md` |\n| Webhooks | `https://orshot.com/docs/integrations/webhooks.md` |\n| Error Reference | `https://orshot.com/docs/error-reference.md` |\n\nWhen a user asks about a specific topic, fetch the relevant `.md` URL for the latest details.\n\n## Getting Started\n\n### Authentication\n\nAll API requests require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer <ORSHOT_API_KEY>\n```\n\n[Get your API key](https://orshot.com/docs/quick-start/get-api-key) from **Workspace Settings → API Keys** in the Orshot dashboard.\n\n### Remote MCP Server\n\nOrshot runs a hosted MCP server so agents (Claude, Cursor, Codex, Windsurf, ChatGPT, etc.) can use Orshot as tools — no local install.\n\n- **URL:** `https://mcp.orshot.com/mcp` (transport: `streamable-http`)\n- **Auth:** OAuth 2.0 (the client walks you through it), or send an Orshot API key as a Bearer token\n- **Claude Code:** `claude mcp add --transport http orshot https://mcp.orshot.com/mcp`\n- **Cursor / Windsurf / VS Code / ChatGPT / Codex:** add the URL as a remote/HTTP MCP server\n\nIt exposes tools for rendering, studio + library templates, brand assets, workflows, social accounts, and workspace/logs. Full discovery doc: `https://orshot.com/.well-known/mcp.json`. Setup guide: `https://orshot.com/docs/integrations/mcp-server.md`.\n\n### SDKs\n\n#### Node.js\n\n```bash\nnpm install orshot\n```\n\n```js\nimport { Orshot } from \"orshot\";\nconst orshot = new Orshot(\"<ORSHOT_API_KEY>\");\n\n// Render from template\nconst response = await orshot.renderFromTemplate({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello World\" },\n  responseType: \"base64\", // \"base64\" | \"url\" | \"binary\"\n  responseFormat: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\"\n});\n\n// Generate signed URL\nconst signedUrl = await orshot.generateSignedUrl({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello\" },\n  expiresAt: 1744276943,\n  renderType: \"images\",\n  responseFormat: \"png\",\n});\n```\n\n#### Python\n\n```bash\npip install orshot\n```\n\n```python\nimport orshot\nos = orshot.Orshot('<ORSHOT_API_KEY>')\n\nresponse = os.render_from_template({\n  'template_id': 'open-graph-image-1',\n  'modifications': {'title': 'Hello World'},\n  'response_type': 'base64',\n  'response_format': 'png'\n})\n```\n\n#### Other SDKs\n\n- **PHP:** `composer require rishimohan/orshot`\n- **Ruby:** `gem install orshot`\n\n## Common Gotchas (read before rendering)\n\nThe mistakes that most often make Orshot calls fail or return the wrong output:\n\n1. **Modifications key by `parameterId`, not layer name.** `modifications: { \"headline\": \"...\" }` targets the element whose `parameterId` is `headline`. If a value is ignored, the key is wrong — fetch the template's modifications (`GET /v1/studio/templates/:id`) to see the real keys instead of guessing.\n2. **Multi-page templates need a `pageN@` prefix.** Use `\"page1@title\"`, `\"page2@title\"`. A bare `\"title\"` only affects page 1.\n3. **Studio `templateId` is an integer; utility `templateId` is a string.** `/v1/studio/render` takes an integer ID; `/v1/generate/:renderType` takes a string slug like `\"website-screenshot\"`.\n4. **Video output requires video elements in the template.** Requesting `format: \"mp4\"` on an image-only template fails — the template must contain at least one video element.\n5. **Image/video URLs in modifications must be publicly reachable.** The renderer fetches them server-side, so `localhost`, expired signed URLs, or auth-gated URLs won't load.\n6. **Style overrides use dot notation on the parameterId** — `\"title.fontSize\": \"48px\"`, not a nested object.\n7. **Response shape differs by page count:** single page → `data` is an object (read `data.content`); multi-page/carousel → `data` is an array of `{ page, content }`. Handle both.\n8. **`base64` and `binary` response types don't combine** — `binary` returns one raw file stream.\n9. **Credits vs AI Credits are separate** — normal renders spend credits; `.prompt` (AI) modifications additionally spend AI Credits.\n10. **On `429`, back off** using the `Retry-After` response header.\n\nWhen unsure about a template's parameters, always fetch its modifications first.\n\n## Template Architecture\n\nThis section describes the complete structure of an Orshot template for MCP tools and AI agents.\n\n### Template Structure\n\nAn Orshot template consists of **pages**, each containing a **canvas** and **elements**.\n\n```\nTemplate\n├── id: number | string\n├── name: string\n├── description: string\n├── width: number\n├── height: number\n├── pages_data: Array\n    └── Page\n        ├── id: string (UUID)\n        ├── name: string\n        ├── canvas: CanvasConfig\n            ├── width: number\n            ├── height: number\n            ├── backgroundColor: string\n            ├── backgroundImage: string\n        ├── elements: Element[]\n        ├── modifications: Modification[] (API parameters)\n            ├── id: string\n            ├── type: string\n            ├── element: Element\n            ├── description: string\n        └── thumbnail_url: string | null\n```\n\n### Canvas Configuration\n\n| Property          | Type   | Default         | Description                               |\n| ----------------- | ------ | --------------- | ----------------------------------------- |\n| `width`           | number | 800             | Canvas width in pixels (max: 5000)        |\n| `height`          | number | 800             | Canvas height in pixels (max: 5000)       |\n| `backgroundColor` | string | \"#ffffff\"       | Background color (hex, rgba, or gradient) |\n| `backgroundImage` | string | \"\"              | URL to background image                   |\n| `borderWidth`     | number | 0               | Border width in pixels                    |\n| `borderColor`     | string | \"rgba(0,0,0,1)\" | Border color                              |\n| `borderStyle`     | string | \"solid\"         | Border style (solid, dashed, etc)         |\n\n### Canvas Size Presets\n\n| Name                 | Dimensions | Use Case                        |\n| -------------------- | ---------- | ------------------------------- |\n| Square               | 1080×1080  | Instagram posts, general social |\n| Instagram Story      | 1080×1920  | Stories, Reels, TikTok          |\n| Slide/Presentation   | 1920×1080  | Presentations, slides           |\n| YouTube Thumbnail    | 1280×720   | Video thumbnails                |\n| Twitter Post         | 1600×900   | X/Twitter posts                 |\n| Open Graph           | 1200×630   | Link previews, Facebook         |\n| Pinterest Pin        | 1000×1500  | Pinterest                       |\n| A4 Document          | 2480×3508  | Print documents                 |\n| App Store Screenshot | 1290×2796  | iOS app screenshots             |\n\n### Universal Element Properties\n\nAll elements share these base properties:\n\n| Property            | Type    | Description                                  |\n| ------------------- | ------- | -------------------------------------------- |\n| `id`                | string  | Unique identifier (UUID)                     |\n| `name`              | string  | Display name in layer list (was `layerName`) |\n| `type`              | string  | \"text\", \"image\", \"shape\", \"video\"            |\n| `position`          | object  | `{ x: number, y: number }` from top-left     |\n| `dimensions`        | object  | `{ width: number, height: number }`          |\n| `rotation`          | number  | Rotation in degrees (0-360)                  |\n| `zIndex`            | number  | Layer order (higher = on top)                |\n| `aspectRatioLocked` | boolean | Lock aspect ratio during resize              |\n| `isHidden`          | boolean | Hide element from render                     |\n| `skewX`             | number  | Horizontal skew angle                        |\n| `skewY`             | number  | Vertical skew angle                          |\n\n### Text Element\n\n**Content Types:**\n\n- **Plain text**: `\"Hello World\"` - Standard text string\n- **Multi-line**: Use `\\n` for line breaks: `\"Line 1\\nLine 2\"`\n- **Dynamic via API**: Use `.prompt` modifier for AI-generated text\n\n```javascript\n{\n  type: \"text\",\n  content: string,          // Plain text string\n  layerName: string,        // Display name\n  zIndex: number,\n  rotation: number,\n  position: { x: number, y: number },\n  dimensions: { width: number, height: number },\n  style: {\n    // Typography\n    fontFamily: string,     // e.g., \"Inter\", \"Prata\", \"SF Pro\"\n    fontSize: string,       // e.g., \"48px\"\n    fontWeight: string | number, // \"400\", \"700\", 700\n    fontStyle: string,      // \"normal\", \"italic\"\n    lineHeight: number,     // e.g., 1.2\n    letterSpacing: string,  // e.g., \"0px\", \"2px\"\n\n    // Appearance\n    fill: string,           // Color or gradient\n    color: string,          // Hex or rgba\n    opacity: number,        // 0-1\n    stroke: string,         // Stroke color\n    strokeWidth: string,    // e.g., \"0px\"\n\n    // Alignment & Layout\n    textAlign: string,      // \"left\", \"center\", \"right\"\n    verticalAlign: string,  // \"flex-start\", \"center\", \"flex-end\"\n    textTransform: string,  // \"none\", \"uppercase\"\n    textDecoration: string, // \"none\", \"underline\"\n    textMode: string,       // \"overflow\", \"fit\"\n    paddingX: string,\n    paddingY: string,\n\n    // Borders & Backgrounds\n    borderColor: string,\n    borderWidth: string,\n    borderRadius: string,\n    textBackgroundColor: string,\n    textBackgroundRadius: string,\n    textStrokeColor: string,\n    textStrokeWidth: string,\n\n    // Effects\n    minFontSize: string,    // For \"fit\" mode\n    filter: string,         // e.g., \"blur(0px)\"\n    mixBlendMode: string,   // \"normal\", \"multiply\", etc.\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  // Parameterization\n  parameterizable: boolean,\n  parameterId: string,\n  parameterType: \"text\"\n}\n```\n\n**Gradient text:**\n\n```javascript\ncolor: \"linear-gradient(90deg, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Image Element\n\n**Content Types:**\n\n- **URL** (recommended): `\"https://example.com/image.png\"` - Best for dynamic content\n- **Base64**: `\"data:image/png;base64,iVBORw0KGgo...\"` - For embedded images\n- **Binary**: Raw binary data (API upload only)\n\n```javascript\n{\n  type: \"image\",\n  content: string,          // URL (preferred), base64, or binary\n  isSvg: boolean,\n  layerName: string,\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string, // \"center\", \"top left\"\n\n    // Appearance\n    opacity: number,\n    fill: string,           // Background fill\n    stroke: string,         // Border stroke\n\n    // Borders\n    borderRadius: string,   // \"0px\", \"12px\", \"50%\"\n    borderWidth: string,\n    borderColor: string,\n\n    // Effects\n    filter: string,         // \"blur(2px)\", \"grayscale(100%)\"\n    mixBlendMode: string,\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string,\n\n    svgColor: string        // Recolor monochrome SVGs\n  },\n  parameterType: \"imageUrl\"\n}\n```\n\n### Shape Element\n\n```javascript\n{\n  type: \"shape\",\n  shapeType: string,        // \"rectangle\", \"circle\", \"arrow\"\n  layerName: string,\n  style: {\n    // Fill & Stroke\n    fill: string,           // Color or gradient\n    stroke: string,\n    strokeWidth: string,    // e.g. \"0px\"\n\n    // Dimensions\n    borderRadius: string,   // Rectangle only\n    borderWidth: string,\n    borderColor: string,\n    borderStyle: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Shadows\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  parameterType: \"fill\"\n}\n```\n\n**Gradient fills:**\n\n```javascript\nfill: \"linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%)\";\nfill: \"radial-gradient(circle at center, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Video Element\n\n**Content Types:**\n\n- **URL** (required): `\"https://example.com/video.mp4\"` - Must be a publicly accessible URL\n- Supported formats: MP4, WebM, MOV\n- For best results, use MP4 with H.264 codec\n\n```javascript\n{\n  type: \"video\",\n  content: string,          // Video URL (must be publicly accessible)\n  videoOptions: {\n    loop: boolean,\n    muted: boolean,\n    trim_start_time: string,\n    trim_end_time: string,\n    duration: number | null\n  },\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Borders & Shadows\n    borderRadius: string,\n    borderWidth: string,\n    borderColor: string,\n    elementBoxShadowX: string,\n    elementBoxShadowY: string,\n    elementBoxShadowBlur: string,\n    elementBoxShadowColor: string\n  },\n  parameterType: \"videoUrl\"\n}\n```\n\n### Parameterization Best Practices\n\nWhen creating or updating templates, **always ensure all text, image, and video elements are parameterizable** with unique IDs. This enables dynamic content replacement via the API.\n\n#### Required Setup\n\nEvery dynamic element MUST have:\n\n```javascript\n{\n  parameterizable: true,\n  parameterId: \"unique_id\",  // Unique across template, lowercase with underscores\n  parameterType: \"text\" | \"imageUrl\" | \"videoUrl\"\n}\n```\n\n#### Naming Conventions\n\n| Element Type | parameterId Examples                        | parameterType |\n| ------------ | ------------------------------------------- | ------------- |\n| Text         | `headline`, `subtitle`, `cta_text`, `price` | `\"text\"`      |\n| Image        | `product_image`, `logo`, `background_image` | `\"imageUrl\"`  |\n| Video        | `hero_video`, `background_video`            | `\"videoUrl\"`  |\n\n#### Best Practices\n\n1. **Use descriptive IDs:** `product_title` not `text1`\n2. **Be consistent:** Use snake_case across all templates\n3. **Unique per template:** No duplicate parameterIds on same page\n4. **Group logically:** Related elements share naming prefix (e.g., `card_title`, `card_image`)\n\n#### Validation Checklist\n\nBefore finalizing any template update:\n- [ ] All text elements have `parameterizable: true` and unique `parameterId`\n- [ ] All image elements have `parameterizable: true` and unique `parameterId`\n- [ ] All video elements have `parameterizable: true` and unique `parameterId`\n- [ ] No duplicate parameterIds exist on the same page\n- [ ] parameterIds are descriptive and follow snake_case convention\n\n### Design Best Practices\n\n#### Typography Guidelines\n- **Font limit:** Use 2-3 fonts maximum per template\n- **Hierarchy:** Headings should be 1.5-2x larger than body text\n- **Minimum size:** 24px for social media readability\n- **Weights:** Headings 600-900 (bold), Body 400-500 (regular)\n\n**Popular font pairings:**\n- `Prata` + `Inter`\n- `Instrument Serif` + `DM Sans`\n- `Playfair Display` + `Lato`\n- `Montserrat` + `Open Sans`\n\n**Platform-specific fonts:**\n- iOS: `SF Pro Display`, `SF Pro Text`\n- Android: `Google Sans`, `Roboto`\n\n#### Color Guidelines\n**Luxury/Gold palette:**\n- Gold: `#D4AF37`\n- Dark gold: `#B8860B`\n- Light gold: `#F5E7A3`\n\n**iOS system colors:**\n- Blue: `#007AFF`\n- Gray: `#8E8E93`\n- Background: `#F5F5F7`\n\n**Professional dark:**\n- Navy: `#0F172A`\n- Slate: `#1E293B`, `#334155`\n- Muted: `#64748B`, `#94A3B8`\n\n**Best practices:**\n- Ensure 4.5:1 minimum contrast for text readability\n- Use gradients sparingly for premium effects\n- Consistent color palette (3-5 colors max)\n\n#### Layout Guidelines\n- **Edge padding:** 40-60px from canvas edges\n- **Element spacing:** 20-40px between elements\n- **Alignment:** Center for formal, left for modern\n- **Visual flow:** Guide eye with size, color, position\n\n**zIndex ordering:**\n- Background images/colors: 1\n- Overlay shapes: 2-3\n- Text elements: 4-6\n- Interactive elements: 7+\n\n#### Common Operations (Design Automation)\n\n**Add text element:**\n```javascript\naddElement(\"text\", {\n  content: \"Hello World\",\n  fontFamily: \"Inter\",\n  fontSize: 48,\n  fontWeight: \"700\",\n  color: \"#FFFFFF\",\n  x: 100,\n  y: 100,\n  width: 400,\n  height: 60,\n});\n```\n\n**Add shape backdrop:**\n```javascript\naddElement(\"rectangle\", {\n  fill: \"rgba(0,0,0,0.5)\",\n  width: 1080,\n  height: 200,\n  x: 0,\n  y: 800,\n  borderRadius: \"0px\",\n});\n```\n\n**Batch update multiple elements:**\n```javascript\nbatchUpdate([\n  {\n    elementId: \"heading\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { fontSize: 72 } },\n  },\n  {\n    elementId: \"subtitle\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { color: \"#94A3B8\" } },\n  },\n  { type: \"ORSHOT_UPDATE_CANVAS\", updates: { backgroundColor: \"#0F172A\" } },\n]);\n```\n\n## API Reference\n\n### 1. Render from Studio Template\n\nGenerate images/PDFs/videos from templates designed in Orshot Studio.\n\n**POST** `https://api.orshot.com/v1/studio/render`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123, // Integer - your studio template ID\n    modifications: {\n      title: \"Hello World\",\n      imageUrl: \"https://example.com/photo.jpg\",\n      canvasBackgroundColor: \"#eff2fa\",\n    },\n    response: {\n      type: \"base64\", // \"base64\" | \"url\" | \"binary\"\n      format: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\" | \"mp4\" | \"webm\" | \"gif\"\n      scale: 1, // 1 = original size, 2 = double\n      includePages: [1, 3], // optional – only for multi-page templates\n      fileName: \"my-render\", // optional – custom filename (without extension)\n    },\n    pdfOptions: {\n      // optional – only when format is \"pdf\"\n      margin: \"20px\",\n      rangeFrom: 1,\n      rangeTo: 2,\n      colorMode: \"rgb\", // \"rgb\" or \"cmyk\"\n      dpi: 300,\n    },\n  }),\n});\n```\n\n**Response (single page):**\n```json\n{\n  \"data\": {\n    \"content\": \"data:image/png;base64,iVBORw0.....\",\n    \"format\": \"png\",\n    \"type\": \"base64\",\n    \"responseTime\": 325.22\n  }\n}\n```\n\n**Response (multi-page/carousel):**\n```json\n{\n  \"data\": [\n    { \"page\": 1, \"content\": \"https://storage.orshot.com/.../image1.png\" },\n    { \"page\": 2, \"content\": \"https://storage.orshot.com/.../image2.png\" }\n  ],\n  \"format\": \"png\",\n  \"type\": \"url\",\n  \"responseTime\": 3166.01,\n  \"totalPages\": 2,\n  \"renderedPages\": 2\n}\n```\n\n### 2. Render from Utility Template\n\nGenerate renders from Orshot's pre-built utility templates (e.g., website screenshots, tweet images).\n\n**POST** `https://api.orshot.com/v1/generate/{renderType}`\n\n- `renderType`: `images` or `pdfs`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/generate/images\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\", // String ID for utility templates\n    response: {\n      format: \"png\",\n      type: \"base64\",\n    },\n    modifications: {\n      websiteUrl: \"https://example.com\",\n      fullCapture: false,\n      delay: 500,\n      width: 1200,\n      height: 1000,\n    },\n  }),\n});\n```\n\n### 3. Generate Signed URL\n\nCreate publicly accessible render URLs without exposing your API key.\n\n**POST** `https://api.orshot.com/v1/signed-url/create`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/signed-url/create\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\",\n    expiresAt: 1744550160505, // UNIX timestamp, or null for no expiry\n    renderType: \"images\",\n    modifications: {\n      websiteUrl: \"https://example.com\",\n    },\n  }),\n});\n```\n\n### 4. List Studio Templates\n\n**GET** `https://api.orshot.com/v1/studio/templates/all?page=1&limit=10`\n\nResponse includes `data` array of templates and `pagination` object with `page`, `limit`, `total`, `totalPages`.\n\n### 5. Get Studio Template\n\n**GET** `https://api.orshot.com/v1/studio/templates/:templateId`\n\nReturns the template metadata including available `modifications`.\n\n### 6. Delete Studio Template\n\n**DELETE** `https://api.orshot.com/v1/studio/templates/:templateId`\n\n### 7. Duplicate Studio Template\n\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/duplicate`\n\n### 8. Get Profile & Workspaces\n\n**GET** `https://api.orshot.com/v1/me`\n\nReturns your profile and the workspaces your API key or OAuth token can access, including plan info and credit usage.\n\n### 9. Brand Assets API\n\nBrand assets are grouped by type — **images**, **colors**, **fonts**, **videos**, **audio** — each with its own routes under `/v1/brand-assets/{type}/…`.\n\n#### Images\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/images/get`\n- **Upload:** `POST https://api.orshot.com/v1/brand-assets/images/add` — `multipart/form-data` with a `file` field\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/images/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/images/delete/:id`\n\n#### Colors\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/colors/get`\n- **Add:** `POST https://api.orshot.com/v1/brand-assets/colors/add` — body `{ type: \"hex\" | \"gradient\", value, tags? }`\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/colors/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/colors/delete/:id`\n\n#### Fonts / Videos / Audio\nSame shape — swap the type segment (`fonts`, `videos`, `audio`):\n- **Get:** `GET /v1/brand-assets/{type}/get`\n- **Upload:** `POST /v1/brand-assets/{type}/add` (`multipart/form-data`, `file` field)\n- **Update tags:** `PATCH /v1/brand-assets/{type}/update/:id`\n- **Delete:** `DELETE /v1/brand-assets/{type}/delete/:id`\n\n#### Search\n- **Search assets:** `GET https://api.orshot.com/v1/brand-assets/search`\n\nUse an uploaded asset in a render by passing its hosted URL as an image/video modification value.\n\n### 10. Enterprise API Endpoints\n\nThese endpoints require an Enterprise plan.\n\n#### Create Studio Template\n**POST** `https://api.orshot.com/v1/studio/templates/create`\n\n```js\n{\n  name: \"Product Banner\",        // required, max 255 chars\n  description: \"Banner template\", // optional\n  canvas_width: 1200,             // required, 1-5000\n  canvas_height: 628,             // required, 1-5000\n  pages_data: [...]               // optional - array of page objects with elements\n}\n```\n\n#### Bulk Create Studio Templates\n**POST** `https://api.orshot.com/v1/studio/templates/bulk-create`\nCreate multiple templates at once via CSV or JSON.\n\n#### Update Template\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId`\nUpdate template name and description.\n\n#### Update Template Modifications\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId/update-modifications`\nUpdate text and image content in template layers.\n\n#### Generate Template Variants\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/generate-variants`\nGenerate multiple size variants of a template using AI.\n\n### 11. Dynamic URLs\n\nGenerate images directly from URL parameters:\n\n```\nhttps://api.orshot.com/v1/studio/dynamic-url/my-image?title=Hello%20World&title.fontSize=48px&title.color=%23ff0000\n```\nURL-encode special characters (e.g., `#` → `%23`).\n\n### 12. Template Folders & Sharing\n\nOrganize studio templates into folders and share them:\n\n- **List folders:** `GET https://api.orshot.com/v1/studio/folders`\n- **Create folder:** `POST https://api.orshot.com/v1/studio/folders`\n- **Update folder:** `PATCH https://api.orshot.com/v1/studio/folders/:folderId`\n- **Delete folder:** `DELETE https://api.orshot.com/v1/studio/folders/:folderId`\n- **Move template into folder:** `PATCH https://api.orshot.com/v1/studio/templates/:templateId/folder`\n- **Get template sharing:** `GET https://api.orshot.com/v1/studio/templates/:templateId/share`\n- **Update template sharing:** `POST https://api.orshot.com/v1/studio/templates/:templateId/share`\n\n### 13. Workflows API\n\nWorkflows automate multi-step pipelines (trigger → fetch data → render → publish/deliver). Available to first-party clients (like the Orshot MCP server) and Enterprise workspaces.\n\n- **List workflows:** `GET https://api.orshot.com/v1/workflows`\n- **Create workflow:** `POST https://api.orshot.com/v1/workflows`\n- **Get workflow:** `GET https://api.orshot.com/v1/workflows/:id`\n- **Update workflow:** `PATCH https://api.orshot.com/v1/workflows/:id`\n- **Delete workflow:** `DELETE https://api.orshot.com/v1/workflows/:id`\n- **Run workflow:** `POST https://api.orshot.com/v1/workflows/:id/run`\n- **List runs:** `GET https://api.orshot.com/v1/workflows/:id/runs`\n- **Get a run:** `GET https://api.orshot.com/v1/workflows/:id/runs/:runId`\n- **Validate a workflow:** `POST https://api.orshot.com/v1/workflows/validate`\n- **List available nodes:** `GET https://api.orshot.com/v1/workflows/nodes`\n- **Get/update sharing:** `GET` / `POST https://api.orshot.com/v1/workflows/:id/share`\n\n## Render Configuration\n\n### Dynamic Parameters\n\nOverride template styles, content, and behavior at render time using dot notation.\n\n#### Style Parameters\n\nFormat: `parameterId.property`\n\n```json\n{\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"title.fontSize\": \"48px\",\n    \"title.color\": \"#ff0000\",\n    \"title.fontFamily\": \"Roboto\",\n    \"title.textAlign\": \"center\",\n    \"logo.borderRadius\": \"50%\",\n    \"logo.objectFit\": \"cover\"\n  }\n}\n```\n\n**Text properties:** `fontSize`, `fontWeight`, `fontStyle`, `fontFamily`, `lineHeight`, `letterSpacing`, `textAlign`, `verticalAlign`, `textDecoration`, `textTransform`, `color`, `backgroundColor`, `backgroundRadius`, `textStrokeWidth`, `textStrokeColor`, `opacity`, `filter`, `dropShadowX/Y/Blur/Color`\n\n**Image properties:** `objectFit`, `objectPosition`, `borderRadius`, `borderWidth`, `borderColor`, `boxShadowX/Y/Blur/Color`, `opacity`, `filter`\n\n**Shape properties:** `fill`, `stroke`, `strokeWidth`, `borderRadius`, `opacity`\n\n**Position/Size (all elements):** `x`, `y`, `width`, `height`\n\nProperty names are **case-insensitive**.\n\n#### Multi-Page Templates\n\nPrefix modifications with page number:\n```json\n{\n  \"modifications\": {\n    \"page1@title\": \"Page 1 Title\",\n    \"page2@title\": \"Page 2 Title\",\n    \"page1@title.fontSize\": \"48px\"\n  }\n}\n```\n\n#### AI Content Generation (.prompt)\n\nGenerate text or images using AI:\n```json\n{\n  \"modifications\": {\n    \"headline.prompt\": \"Write a catchy headline about coffee\",\n    \"background.prompt\": \"A serene mountain landscape at sunset\"\n  }\n}\n```\n- `.prompt` on a text element generates copy; on an image element it generates imagery. The underlying AI models are managed by Orshot and may change over time — don't hardcode a specific model. AI modifications consume AI Credits.\n\n#### Interactive Links (.href)\n\nAdd clickable links in PDF outputs:\n```json\n{\n  \"modifications\": {\n    \"cta_button.href\": \"https://example.com/signup\",\n    \"logo.href\": \"https://company.com\"\n  },\n  \"response\": { \"format\": \"pdf\" }\n}\n```\n\n#### Video Parameters\n\nControl video elements dynamically:\n```json\n{\n  \"modifications\": {\n    \"bgVideo\": \"https://example.com/video.mp4\",\n    \"bgVideo.trimStart\": 5,\n    \"bgVideo.trimEnd\": 15,\n    \"bgVideo.muted\": false,\n    \"bgVideo.loop\": true\n  },\n  \"response\": { \"format\": \"mp4\" }\n}\n```\n\n### Video Render Example\n\nRender templates with video elements as MP4, WebM, or GIF:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: {\n      videoElement: \"https://example.com/custom-video.mp4\",\n      \"videoElement.trimStart\": 0,\n      \"videoElement.trimEnd\": 10,\n      \"videoElement.muted\": false,\n      \"videoElement.loop\": true,\n    },\n    videoOptions: {\n      trimStart: 0,\n      trimEnd: 20,\n      muted: true,\n      loop: true,\n    },\n    response: {\n      type: \"url\",\n      format: \"mp4\",\n    },\n  }),\n});\n```\n\n### Smart Resize (one design → any size)\n\nRender a template at a **different canvas size without redesigning it** — the layout is deterministically re-solved to fit (elements re-anchor, backgrounds stretch, groups move together). Set these on the `response` object:\n\n- **`size`** — *replace* the render size. A preset slug (`\"instagram-story\"`, `\"og-image\"`, `\"youtube-thumbnail\"`, …), a `\"WIDTHxHEIGHT\"` string (`\"1080x1920\"`), or use `width` + `height` (10–5000px each).\n- **`extraSizes`** — *add* the same design at extra sizes in one call. An array (`[\"1080x1920\", \"1080x1080\"]`) or a named object (`{ story: \"1080x1920\", square: \"1080x1080\" }`). Each output gains a nested `extraSizes` array of `{ size, width, height, content }`.\n\n```json\n{\n  \"templateId\": 123,\n  \"modifications\": { \"title\": \"Launch Day\" },\n  \"response\": {\n    \"type\": \"url\",\n    \"format\": \"png\",\n    \"size\": \"1200x630\",\n    \"extraSizes\": [\"1080x1920\", \"1080x1080\"]\n  }\n}\n```\n\nImage formats only (`png`/`jpg`/`webp`/`avif`), up to 50 extra outputs per call. Each extra output is billed like a page (1 credit). Saved sizes from the Studio Smart Resize panel reproduce their approved preview exactly.\n\n### Response Types\n\n| Type     | Description                                     |\n| -------- | ----------------------------------------------- |\n| `url`    | Returns a hosted URL to the rendered file       |\n| `base64` | Returns base64-encoded content as a string      |\n| `binary` | Returns binary file content for custom handling |\n\n### Response Formats\n\n| Format | Type  | Notes                                      |\n| ------ | ----- | ------------------------------------------ |\n| `png`  | Image | Best quality, larger size                  |\n| `webp` | Image | Smaller size, good quality                 |\n| `jpg`  | Image | Compressed, no transparency                |\n| `avif` | Image | Smallest size, modern browsers             |\n| `pdf`  | Doc   | Supports multi-page, clickable links, CMYK |\n| `mp4`  | Video | H.264, requires video elements in template |\n| `webm` | Video | VP9, web-optimized                         |\n| `mov`  | Video | QuickTime container                        |\n| `mkv`  | Video | Matroska container                         |\n| `gif`  | Video | Animated, no audio support                 |\n\n### Render Usage & Costs\n\nUsage is measured in **credits**. 1 credit = 1 image, 1 PDF page, or 1 second of video.\n\n| Output               | Cost                     |\n| -------------------- | ------------------------ |\n| Image (PNG/JPG/WebP/AVIF) | 1 credit per image  |\n| PDF                  | 1 credit per page        |\n| Video (MP4/WebM/MOV/MKV/GIF) | 1 credit per second |\n\nMulti-page templates and Smart Resize extra sizes: each output page/size counts as its own credit. AI modifications (`.prompt`) additionally consume AI Credits.\n\n## Common Recipes\n\nEnd-to-end patterns for the jobs people most often automate with Orshot.\n\n### OG image on every deploy\nRender a studio template to a stable hosted URL and drop it into your `<meta>` tags.\n```js\nconst { data } = await fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: post.title, author: post.author },\n    response: { type: \"url\", format: \"png\", size: \"og-image\" },\n  }),\n}).then((r) => r.json());\n// <meta property=\"og:image\" content={data.content} />\n```\n\n### Bulk-generate from a CSV (certificates, badges, invoices)\nLoop rows and render one PDF each. Describe image layers with `.alt` for accessible PDFs.\n```js\nfor (const row of rows) {\n  await fetch(\"https://api.orshot.com/v1/studio/render\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n    body: JSON.stringify({\n      templateId: 456,\n      modifications: { name: row.name, course: row.course, date: row.date },\n      response: { type: \"url\", format: \"pdf\" },\n      pdfOptions: { title: `${row.name} — Certificate` },\n    }),\n  });\n}\n```\n\n### Render and auto-post to social in one call\nAdd a `publish` object (see Social Publishing) — no second request.\n```js\nbody: JSON.stringify({\n  templateId: 123,\n  modifications: { title: \"Launch day!\" },\n  response: { type: \"url\", format: \"png\" },\n  publish: { accounts: [1, 2], content: \"We just shipped 🚀\" },\n});\n```\n\n### One design, every social size (Smart Resize)\n```js\nresponse: { type: \"url\", format: \"png\", extraSizes: [\"1080x1920\", \"1080x1080\", \"1200x630\"] }\n// each output gains a nested extraSizes[] of { size, width, height, content }\n```\n\n### Automate a recurring pipeline (Workflows)\nCreate a workflow (trigger → fetch data → render → publish/deliver) with `POST /v1/workflows`, then trigger it with `POST /v1/workflows/:id/run`. Inspect results via `GET /v1/workflows/:id/runs`. Best driven through the MCP server or the Workflows API on Enterprise/first-party clients.\n\nSee **Setting Up Recurring Automation** below for the full playbook, including how\nto wire it into an n8n/Make/Zapier setup the user already runs.\n\n## Setting Up Recurring Automation\n\nA one-off render is a demo. The value shows up when it runs unattended. Pick the\nroad that matches where the user ALREADY works, not the one easiest to describe.\n\n### Choose the road first\n\nCheck what they already run before pitching anything. `GET /v1/workspace/logs?limit=100`\nreturns a `source` on every row, which answers it factually:\n\n| `source` in their logs | They already run | Lead with |\n| --- | --- | --- |\n| `n8n-integration` | n8n | Add a node to the n8n workflow they have |\n| `orshot-make` | Make | Add a module to their scenario |\n| `zapier-integration` | Zapier | Add an action to their Zap |\n| `orshot-pipedream` | Pipedream | Add a step |\n| `orshot-*-sdk`, `api`, `cli` | Their own code | Write the call into their repo |\n| only `playground` / `orshot-mcp-server` | Nothing yet | Offer Orshot Workflows |\n\nNever pitch a second orchestrator to someone who already has one. A user with a\nlive n8n setup wants this template added to it, not a new tool to learn.\n\nIf you have browser control or repo access, offer to DO the setup rather than\ndescribe it. Whichever road you take, **run it once and show the resulting image\nURL** before calling it done.\n\n### Road A — Orshot Workflows (no orchestrator yet)\n\nOrshot runs the whole loop: trigger, render, deliver. Over MCP:\n\n1. `orshot_suggest_workflows` with the `templateId` — returns automations that\n   fit this template, with draft-ready steps\n2. `orshot_list_workflow_nodes` — use the EXACT node keys it returns. Invented\n   keys (e.g. `render_studio_template`, `slack_send`) fail as `unknown node`;\n   the real keys are `render`, `slack`, and so on\n3. `orshot_list_connected_integrations` — see what is already connected\n4. `orshot_get_workflow_connection_data` — resolve \"my content sheet\" into a\n   concrete id, and confirm the name back to the user\n5. `orshot_validate_workflow` — read `errors`, `warnings` AND `gated`. A `gated`\n   entry means a plan block, not a config problem: the draft still saves, only\n   activation is blocked, so tell the user which plan unlocks it instead of\n   editing steps\n6. `orshot_create_workflow` with `status: \"draft\"`, then share the edit link from\n   the result — it opens pre-configured in their dashboard\n7. `orshot_run_workflow` — prove it works, then they activate\n\n**Scaffolding shapes.** If they have no data source ready, use\n`webhook` → `webhook_body` → `render` → `orshot_url`. It is the only\ntrigger→source pair needing no OAuth AND it validates clean with empty configs,\nso it is runnable immediately and they can POST to it from cron, their app, n8n,\nor Make. A bare `schedule` → `render` is NOT runnable — it fails with \"no data\nsource\"; a schedule needs a source step after it.\n\n### Road B — their existing automation platform\n\nEvery platform needs the same three things: endpoint, auth header, modification\nkeys. Get the exact keys first — never guess them:\n\n```\norshot_get_studio_template_modifications   (or GET /v1/studio/templates/:id/modifications)\n```\n\nThen the call to drop in:\n\n```http\nPOST https://api.orshot.com/v1/studio/render\nAuthorization: Bearer <ORSHOT_API_KEY>\nContent-Type: application/json\n\n{\n  \"templateId\": 1234,\n  \"modifications\": { \"headline\": \"{{ row.title }}\", \"hero_image\": \"{{ row.image_url }}\" },\n  \"response\": { \"type\": \"url\", \"format\": \"png\" }\n}\n```\n\nThe response contains the rendered URL — map it to whatever the next step needs.\n\n- **n8n** — HTTP Request node, POST, header auth, body as above. A dedicated\n  Orshot node also exists in the n8n library.\n- **Make** — HTTP \"Make a request\" module, or the Orshot app modules.\n- **Zapier** — Webhooks by Zapier → POST, or the Orshot Zapier app.\n- **Pipedream** — HTTP step; Orshot components are published.\n- **Own code / cron** — any HTTP client. Use `response.type: \"url\"` in production\n  so you are not moving base64 around, and keep the API key in an environment\n  variable, never inline.\n\nRecurring triggers that work well: a new spreadsheet/Airtable/Notion row, a\nschedule, an inbound webhook, or a CMS publish event.\n\n### Verify before you call it done\n\n1. Trigger one real run\n2. `orshot_list_workspace_logs` (or `GET /v1/workspace/logs`) — the new render\n   appears with the `source` of whatever fired it, proving which system made it\n3. Show the user the image URL from that run\n\nIf the render 403s, read the body: an expired key, an inactive subscription, and\na plan limit all return 403 with different messages. A plan block is not an auth\nproblem — do not retry it as one.\n\n## Integrations & Helps\n\n### Integrations Service Support\nOrshot connects with:\n- **No-code:** Zapier, Make (Integromat), n8n, Pipedream, Airtable\n- **Storage:** Amazon S3, Cloudflare R2, Google Drive, Dropbox\n- **Notifications:** Slack, Webhooks\n- **Design:** Figma Plugin, Canva Import, Polotno Import\n- **CLI:** `npx orshot-cli` for terminal-based generation\n- **MCP Server:** Use with Claude, Cursor, Windsurf via MCP protocol\n- **Embed:** White-label design editor for your app (React SDK, Vue SDK, iframe)\n\n### Common Error Codes\n\n| Code | Error                          | Fix                                          |\n| ---- | ------------------------------ | -------------------------------------------- |\n| 400  | `templateId missing`           | Add `templateId` to request body             |\n| 400  | `Invalid API Key`              | Generate new key from dashboard              |\n| 403  | `Authorization header missing` | Add `Authorization: Bearer <KEY>` header     |\n| 403  | `Subscription inactive`        | Check usage or upgrade plan                  |\n| 403  | `Template not found`           | Verify template ID belongs to your workspace |\n| 403  | `Video on free plan`           | Upgrade to paid plan for video generation    |\n\n### General Best Practices\n\n1. **Use `url` response type** for production – avoids large base64 payloads\n2. **Use `webp` format** for smaller file sizes with good quality\n3. **Test in Playground** before coding – each template has an interactive playground\n4. **Use style parameters** instead of creating multiple template variants\n5. **Use consistent units** – stick to `px` for sizes\n6. **Multi-page prefix** – always use `page1@paramId` format for carousel templates\n7. **Cache renders** – use `?cache=false` query param to bypass cache when needed\n8. **Handle errors** – check for 403/400 status codes and parse error messages\n\n## Social Publishing\n\nPublish rendered images/videos directly to 13+ social platforms via API. Supports Twitter/X, Instagram, LinkedIn, Pinterest, Facebook, TikTok, YouTube, Threads, Bluesky, Reddit, Telegram, Snapchat, and Google Business.\n\nFor detailed setup: fetch `https://orshot.com/docs/publish/introduction.md`\n\n### Connect Accounts\n\nConnect social accounts in **Workspace Settings → Social Accounts** in the Orshot dashboard. Each connected account gets a numeric ID used in API calls.\n\n### Publish from API\n\nAdd the `publish` object to any render request:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: \"New Post\" },\n    response: { type: \"url\", format: \"png\" },\n    publish: {\n      accounts: [1, 3],          // Social account IDs\n      content: \"Check this out!\", // Caption text (max 5000 chars)\n      isDraft: false,             // true = save as draft\n      schedule: {                 // Optional: schedule for later\n        scheduledFor: \"2026-04-25T14:00:00Z\"\n      },\n      timezone: \"America/New_York\",\n      platformOptions: {          // Per-account overrides (keyed by account ID)\n        \"1\": { firstComment: \"https://example.com\" },     // LinkedIn\n        \"3\": { title: \"Pin Title\", link: \"https://...\" }   // Pinterest\n      }\n    }\n  }),\n});\n```\n\n**Response includes publish status:**\n```json\n{\n  \"data\": { \"content\": \"https://storage.orshot.com/...\" },\n  \"publish\": [\n    { \"platform\": \"twitter\", \"username\": \"acme\", \"status\": \"published\", \"url\": \"https://x.com/...\" },\n    { \"platform\": \"linkedin\", \"username\": \"acme\", \"status\": \"scheduled\" }\n  ]\n}\n```\n\n**Format compatibility:**\n- PDF cannot be published to any platform\n- Video (mp4/webm/gif) cannot be published to Google Business\n- Image (png/jpg/webp) cannot be published to TikTok or YouTube (video-only)\n\n## Developer Apps & OAuth\n\nBuild third-party integrations with Orshot using OAuth 2.0. Register apps, authenticate users, and access their workspaces programmatically.\n\nFor detailed setup: fetch `https://orshot.com/docs/developers/oauth-overview.md`\n\n### Register an App\n\n1. Go to **Workspace Settings → Developer Apps** in Orshot dashboard\n2. Create a new app with name, redirect URI, and required scopes\n3. Receive `client_id` and `client_secret`\n\n### OAuth 2.0 Flows\n\n**Authorization Code Flow** (web apps):\n```\nGET https://orshot.com/oauth/authorize?\n  response_type=code&\n  client_id=YOUR_CLIENT_ID&\n  redirect_uri=https://yourapp.com/callback&\n  scope=workspace:templates:read workspace:templates:write render:generate\n```\n\n**Device Flow** (CLI/headless):\n```\nPOST https://orshot.com/oauth/device/code\n  client_id=YOUR_CLIENT_ID&\n  scope=workspace:templates:read render:generate\n```\n\n### Token Exchange\n\n```\nPOST https://orshot.com/oauth/token\n  grant_type=authorization_code&\n  code=AUTH_CODE&\n  client_id=YOUR_CLIENT_ID&\n  client_secret=YOUR_CLIENT_SECRET&\n  redirect_uri=https://yourapp.com/callback\n```\n\n### Available Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `workspace:read` | List and read workspace details |\n| `workspace:templates:read` | List and read templates |\n| `workspace:templates:write` | Update template modifications via API |\n| `render:generate` | Generate images, PDFs, and videos |\n| `mcp:access` | Access via Model Context Protocol |\n| `offline_access` | Long-lived refresh tokens |\n\nFor endpoint details: fetch `https://orshot.com/docs/developers/oauth-endpoints.md`\n\n## White-Label Embed (Orshot Embed)\n\nEmbed Orshot's template editor into your application as a white-label component. Users can design and customize templates directly in your app.\n\nFor detailed setup: fetch `https://orshot.com/docs/orshot-embed/introduction.md`\n\n### Quick Setup (iframe)\n\n```html\n<iframe\n  src=\"https://orshot.com/embeds/YOUR_EMBED_ID?userId=USER_123\"\n  width=\"100%\"\n  height=\"600\"\n  frameborder=\"0\"\n></iframe>\n```\n\n### React SDK\n\n```bash\nnpm install @orshot/react\n```\n\n```jsx\nimport { OrshotEmbed } from \"@orshot/react\";\n\n<OrshotEmbed\n  embedId=\"YOUR_EMBED_ID\"\n  userId=\"user_123\"\n  token=\"JWT_TOKEN\"\n  onRender={(data) => console.log(\"Rendered:\", data)}\n  onSave={(data) => console.log(\"Saved:\", data)}\n/>\n```\n\n### Vue SDK\n\n```bash\nnpm install @orshot/vue\n```\n\n```vue\n<template>\n  <OrshotEmbed\n    embed-id=\"YOUR_EMBED_ID\"\n    user-id=\"user_123\"\n    @render=\"onRender\"\n    @save=\"onSave\"\n  />\n</template>\n```\n\n### Key Features\n\n- **Per-user templates**: Pass `userId` to give each user their own template copies\n- **JWT authentication**: Secure embed access with domain whitelist validation\n- **Webhooks**: Get notified on render completion, template save, etc.\n- **Custom buttons**: Add your own action buttons to the editor toolbar\n- **PostMessage API**: Control the embed programmatically from your app\n\nFor React SDK details: fetch `https://orshot.com/docs/orshot-embed/react-sdk.md`\nFor Vue SDK details: fetch `https://orshot.com/docs/orshot-embed/vue-sdk.md`\nFor webhook setup: fetch `https://orshot.com/docs/orshot-embed/webhooks.md`\n\n## Migrating from Other Platforms\n\nThis section maps authentication, endpoints, and request formats for migrating from competing platforms. For detailed comparison articles, fetch the corresponding blog post URL.\n\n### Migration Playbook (any platform)\n\nThe API-call swap is the easy part (mapped per-platform below). The real work is recreating the design, because you can't import another platform's template JSON:\n\n1. **Recreate the template in Orshot Studio** (or via `POST /v1/studio/templates/create`). Rebuild the layout, then mark every dynamic layer parameterizable with a clear `parameterId`.\n2. **Map old field names → Orshot `parameterId`s.** Other platforms key by layer name, `image_url`, `payload`, etc.; Orshot keys by `parameterId`. Keep a lookup from old names to new ones.\n3. **Swap the API call** using the tables below — almost always `Authorization: Bearer` + `POST /v1/studio/render`.\n4. **Handle sync vs async.** Most platforms are async (submit → poll/webhook); Orshot renders **synchronously**, so delete the polling/webhook code and read `data.content` from the response.\n5. **Verify the response shape** — single page is an object, multi-page is an array (Common Gotchas #7) — before cutting traffic over.\n\n### Migrating from BannerBear\n\nBlog: `https://orshot.com/blog/bannerbear-api-alternative.md`\n\n**Authentication:**\n| BannerBear | Orshot |\n|------------|--------|\n| `Authorization: Bearer BB_API_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | BannerBear | Orshot |\n|--------|-----------|--------|\n| Generate image | `POST /v2/images` (async, returns 202) | `POST /v1/studio/render` (sync) |\n| Generate image (sync) | `POST sync.api.bannerbear.com/v2/images` (10s timeout) | `POST /v1/studio/render` (sync by default) |\n| List templates | `GET /v2/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |\n| Get template | `GET /v2/templates/:uid` | `GET /v1/studio/templates/:id` |\n| Generate video | `POST /v2/videos` | `POST /v1/studio/render` with `format: \"mp4\"` |\n| Multi-template batch | `POST /v2/collections` | Loop `POST /v1/studio/render` per template |\n\n**Request Format Translation:**\n\nBannerBear uses a `modifications` **array** with named layers:\n```json\n// BannerBear\n{\n  \"template\": \"TEMPLATE_UID\",\n  \"modifications\": [\n    { \"name\": \"title\", \"text\": \"Hello World\" },\n    { \"name\": \"hero\", \"image_url\": \"https://example.com/img.jpg\" },\n    { \"name\": \"bg\", \"color\": \"#FF0000\" }\n  ]\n}\n\n// Orshot equivalent\n{\n  \"templateId\": 123,\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"hero\": \"https://example.com/img.jpg\",\n    \"canvasBackgroundColor\": \"#FF0000\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"png\" }\n}\n```\n\n**Key differences:**\n- BannerBear modifications is an **array**, Orshot is a **flat object**\n- BannerBear uses `image_url` for images, Orshot uses the parameter ID directly with a URL value\n- BannerBear requires polling for async results, Orshot returns synchronously\n- BannerBear `template` is a UID string, Orshot `templateId` is an integer for studio templates\n- Orshot supports style overrides via dot notation (e.g., `\"title.fontSize\": \"48px\"`) — BannerBear does not\n\n### Migrating from Placid\n\nBlog: `https://orshot.com/blog/placid-api-alternative.md`\n\n**Authentication:**\n| Placid | Orshot |\n|--------|--------|\n| `Authorization: Bearer PLACID_TOKEN` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | Placid | Orshot |\n|--------|--------|--------|\n| Generate image | `POST /api/rest/{template_uuid}` | `POST /v1/studio/render` |\n| Get image status | `GET /api/rest/images/{id}` | Not needed (sync response) |\n| List templates | `GET /api/rest/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |\n| Get template | `GET /api/rest/templates/{uuid}` | `GET /v1/studio/templates/:id` |\n| Delete render | `DELETE /api/rest/images/{id}` | Not needed (renders don't expire) |\n\n**Request Format Translation:**\n\nPlacid uses a `layers` **object** with type-specific properties:\n```json\n// Placid\n{\n  \"layers\": {\n    \"title\": {\n      \"text\": \"Hello World\",\n      \"text_color\": \"#FF0000\",\n      \"font\": \"Arial\"\n    },\n    \"hero_image\": {\n      \"image\": \"https://example.com/img.jpg\"\n    },\n    \"background\": {\n      \"background_color\": \"#000000\"\n    }\n  },\n  \"modifications\": {\n    \"width\": 1200,\n    \"height\": 630,\n    \"image_format\": \"jpg\"\n  }\n}\n\n// Orshot equivalent\n{\n  \"templateId\": 123,\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"title.color\": \"#FF0000\",\n    \"title.fontFamily\": \"Arial\",\n    \"hero_image\": \"https://example.com/img.jpg\",\n    \"canvasBackgroundColor\": \"#000000\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"jpg\" }\n}\n```\n\n**Key differences:**\n- Placid separates `layers` (content) from `modifications` (output settings), Orshot combines both in `modifications`\n- Placid uses `text_color`, Orshot uses dot notation `parameterId.color`\n- Placid uses `image` key for image URLs, Orshot uses the parameter ID directly\n- Placid requires polling or webhooks, Orshot returns synchronously\n- Placid renders can expire, Orshot renders persist\n- Placid credits vary by resolution, Orshot is a flat 1 credit per image regardless of size\n\n### Migrating from Creatomate\n\nBlog: `https://orshot.com/blog/creatomate-api-alternative.md`\n\n**Authentication:**\n| Creatomate | Orshot |\n|------------|--------|\n| `Authorization: Bearer CREATOMATE_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | Creatomate | Orshot |\n|--------|-----------|--------|\n| Generate render | `POST /v1/renders` | `POST /v1/studio/render` |\n| Get render status | `GET /v1/renders/:id` | Not needed (sync response) |\n| List templates | `GET /v1/templates` | `GET /v1/studio/templates/all?page=1&limit=10` |\n| Get template | `GET /v1/templates/:id` | `GET /v1/studio/templates/:id` |\n\n**Request Format Translation:**\n\nCreatomate uses a flat `modifications` object (closest to Orshot's format):\n```json\n// Creatomate\n{\n  \"template_id\": \"TEMPLATE_UUID\",\n  \"modifications\": {\n    \"Title\": \"Hello World\",\n    \"Image-1\": \"https://example.com/img.jpg\"\n  },\n  \"output_format\": \"jpg\",\n  \"render_scale\": 1,\n  \"max_width\": 1080\n}\n\n// Orshot equivalent\n{\n  \"templateId\": 123,\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"image_1\": \"https://example.com/img.jpg\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"jpg\", \"scale\": 1 }\n}\n```\n\n**Key differences:**\n- Creatomate `template_id` is a UUID string, Orshot `templateId` is an integer\n- Creatomate element names are display names (e.g., \"Title\", \"Image-1\"), Orshot uses snake_case parameterIds\n- Creatomate has `output_format` at root level, Orshot uses `response.format`\n- Creatomate requires polling/webhooks, Orshot returns synchronously\n- Creatomate renders can expire, Orshot renders persist\n- Orshot supports style overrides via dot notation — Creatomate requires modifying the template source JSON\n- Creatomate gates its preview SDK to a higher tier; Orshot's embed is available on all paid plans\n\n### Migrating from RenderForm\n\nBlog: `https://orshot.com/blog/renderform-api-alternative.md`\n\n**Authentication:**\n| RenderForm | Orshot |\n|------------|--------|\n| `X-API-KEY: RENDERFORM_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | RenderForm | Orshot |\n|--------|-----------|--------|\n| Generate image | `POST /api/v2/render` | `POST /v1/studio/render` |\n| List templates | `GET /api/v2/my-templates?page=1&size=50` | `GET /v1/studio/templates/all?page=1&limit=10` |\n| Get template | `GET /api/v2/my-templates/:id` | `GET /v1/studio/templates/:id` |\n| URL-based render | `GET /img/TEMPLATE.jpg?key=...&param=...` | `GET /v1/studio/dynamic-url/TEMPLATE?param=...` |\n\n**Request Format Translation:**\n\nRenderForm uses dot notation in a `data` object (similar to Orshot's style overrides):\n```json\n// RenderForm\n{\n  \"template\": \"TEMPLATE_ID\",\n  \"data\": {\n    \"title.text\": \"Hello World\",\n    \"hero.src\": \"https://example.com/img.jpg\",\n    \"bg.color\": \"#FF0000\"\n  },\n  \"fileName\": \"output\",\n  \"width\": 1200,\n  \"height\": 630\n}\n\n// Orshot equivalent\n{\n  \"templateId\": 123,\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"hero\": \"https://example.com/img.jpg\",\n    \"canvasBackgroundColor\": \"#FF0000\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"png\", \"fileName\": \"output\" }\n}\n```\n\n**Key differences:**\n- RenderForm uses `X-API-KEY` header, Orshot uses `Authorization: Bearer`\n- RenderForm uses `data` with `componentId.property` dot notation for everything, Orshot uses `modifications` with parameter IDs for content and dot notation only for style overrides\n- RenderForm `template` is a string ID, Orshot `templateId` is an integer\n- RenderForm images can expire, Orshot renders persist\n- Orshot's free tier has no watermarks\n\n### Migrating from Abyssale\n\nBlog: `https://orshot.com/blog/abyssale-api-alternative.md`\n\n**Authentication:**\n| Abyssale | Orshot |\n|----------|--------|\n| `x-api-key: ABYSSALE_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | Abyssale | Orshot |\n|--------|---------|--------|\n| Generate image | `POST /async/banner-builder/{designId}/generate` (async) | `POST /v1/studio/render` (sync) |\n| Poll status | `GET /generation-request/{requestId}` | Not needed (sync response) |\n| List designs | `GET /designs` | `GET /v1/studio/templates/all?page=1&limit=10` |\n| Get design | `GET /designs/{designId}` | `GET /v1/studio/templates/:id` |\n| Multi-format render | `template_format_names` param | Loop `POST /v1/studio/render` per format, or use Variant Generation API |\n\n**Request Format Translation:**\n\nAbyssale uses an `elements` object with `payload` for text:\n```json\n// Abyssale\n{\n  \"template_format_names\": [\"facebook-feed\", \"instagram-post\"],\n  \"elements\": {\n    \"title\": { \"payload\": \"Hello World\" },\n    \"hero_image\": { \"image_url\": \"https://example.com/img.jpg\" },\n    \"background\": { \"color\": \"#FF0000\" }\n  },\n  \"callback_url\": \"https://webhook.example.com\"\n}\n\n// Orshot equivalent\n{\n  \"templateId\": 123,\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"hero_image\": \"https://example.com/img.jpg\",\n    \"canvasBackgroundColor\": \"#FF0000\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"png\" }\n}\n```\n\n**Key differences:**\n- Abyssale uses `payload` for text content, Orshot uses the parameter ID directly\n- Abyssale uses `image_url` for images, Orshot uses the parameter ID with URL value\n- Abyssale supports multi-format in single call via `template_format_names`, Orshot requires separate calls or Variant Generation API\n- Abyssale is async-only (polling/webhook), Orshot returns synchronously\n- Abyssale charges per-seat, Orshot has unlimited team members on all plans\n- Abyssale results can expire, Orshot renders persist\n\n### Migrating from DynaPictures\n\nBlog: `https://orshot.com/blog/dynapictures-api-alternative.md`\n\n**Authentication:**\n| DynaPictures | Orshot |\n|--------------|--------|\n| `Authorization: Bearer DYNA_KEY` | `Authorization: Bearer ORSHOT_API_KEY` |\n\n**Endpoint Mapping:**\n| Action | DynaPictures | Orshot |\n|--------|-------------|--------|\n| Generate image | `POST /designs/{id}` | `POST /v1/studio/render` |\n| List designs | `GET /designs` | `GET /v1/studio/templates/all?page=1&limit=10` |\n\n**Key differences:**\n- DynaPictures uses an outdated editor interface, Orshot has a modern Figma-like editor\n- No multi-page support in DynaPictures\n- No white-label editor option\n- Limited integrations compared to Orshot's 15+\n\n### Migrating from Contentdrips\n\nBlog: `https://orshot.com/blog/contentdrips-api-alternative.md`\n\nContentdrips is a social media design tool with API as a secondary feature. Migration is straightforward since Orshot is API-first.\n\n**Key differences:**\n- Contentdrips API is bolted-on, Orshot is API-first\n- Contentdrips has limited dynamic parameter capabilities\n- No white-label editor, no MCP server, no CLI\n- Per-seat pricing vs. Orshot's credit-based pricing\n\n### Quick Migration Reference\n\n| Feature | BannerBear | Placid | Creatomate | RenderForm | Abyssale | Orshot |\n|---------|-----------|--------|------------|------------|----------|--------|\n| **Auth header** | `Authorization: Bearer` | `Authorization: Bearer` | `Authorization: Bearer` | `X-API-KEY` | `x-api-key` | `Authorization: Bearer` |\n| **Modifications format** | Array of objects | `layers` object | Flat object | `data` with dot notation | `elements` with `payload` | Flat object |\n| **Template ID type** | UID string | UUID string | UUID string | String | UUID string | Integer (studio) |\n| **Response model** | Async (poll) | Async (poll) | Async (poll) | Sync | Async (poll) | Sync |\n| **Render persistence** | Permanent | Can expire | Can expire | Can expire | Can expire | Permanent |\n| **Style overrides** | No | Limited | Via source JSON | Dot notation | No | Dot notation |\n| **Multi-page** | No | No | No | No | No | Yes |\n| **Video support** | Yes (extra cost) | Yes (extra cost) | Yes | No | Animated only | Yes (1 credit/sec) |\n| **White-label editor** | No | No | Higher tier | No | No | All paid plans |\n| **Social publishing** | No | No | No | No | No | 13+ platforms |\n\n### Links\n\n- Documentation: https://orshot.com/docs\n- API Reference: https://orshot.com/docs/api-reference\n- Integrations: https://orshot.com/integrations\n- MCP Server: https://orshot.com/docs/integrations/mcp-server\n- Templates: https://orshot.com/templates\n- Pricing: https://orshot.com/pricing\n- Developer Apps: https://orshot.com/docs/developers\n- Social Publishing: https://orshot.com/docs/publish/introduction\n- Orshot Embed: https://orshot.com/docs/orshot-embed/introduction\n"
AFTER
"---\nname: orshot\ndescription: Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render jobs) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.\nmetadata:\n  author: Rishi Mohan\n  version: \"1.3.0\"\n---\n\n# Orshot – Automated Visual Content Generation\n\n[Orshot](https://orshot.com) is an automated image, PDF, and video generation platform. Design templates in Orshot Studio (or import from Canva/Figma), then generate renders via REST API, SDKs, or no-code integrations.\n\n- **Documentation:** https://orshot.com/docs\n- **API Base URL:** https://api.orshot.com/v1\n\n## When to Use This Skill\n\nUse this skill whenever the user mentions **Orshot**, or when the task is:\n\n- Generating images, PDFs, or videos programmatically from templates\n- Building automated marketing visual pipelines (OG images, ad creatives, thumbnails)\n- Creating dynamic social media content (carousels, posts, stories) and publishing it\n- Generating certificates, invoices, tickets, or reports as PDFs\n- Building image/PDF/video generation into a product\n- Automating visuals with Zapier, Make, n8n, Airtable, or the CLI\n- Connecting Orshot to an AI agent or MCP client (Claude, Cursor, Codex, Windsurf, ChatGPT)\n- Embedding a white-label design editor into an app\n- Migrating from another template-based image/PDF/video generation API\n\n**Don't use this skill** for generating standalone AI images with no template or Orshot involved (a one-off \"make me an image\" request) — that isn't what Orshot does.\n\n## Accessing Detailed Documentation\n\nAny page on orshot.com can be fetched as clean markdown by appending `.md` to the URL or sending `Accept: text/markdown` header. Use this to get detailed, up-to-date information on demand without relying solely on this skill file.\n\n**Examples:**\n```\nhttps://orshot.com/docs/api-reference.md\nhttps://orshot.com/docs/sdks/node.md\nhttps://orshot.com/docs/publish/publish-from-api.md\nhttps://orshot.com/docs/developers/oauth-overview.md\nhttps://orshot.com/docs/orshot-embed/introduction.md\n```\n\n**Key documentation pages:**\n| Topic | URL |\n|-------|-----|\n| API Reference | `https://orshot.com/docs/api-reference.md` |\n| Node.js SDK | `https://orshot.com/docs/sdks/node.md` |\n| Python SDK | `https://orshot.com/docs/sdks/python.md` |\n| PHP SDK | `https://orshot.com/docs/sdks/php.md` |\n| Ruby SDK | `https://orshot.com/docs/sdks/ruby.md` |\n| Studio Templates | `https://orshot.com/docs/orshot-studio/introduction.md` |\n| Style Parameters | `https://orshot.com/docs/orshot-studio/style-parameters.md` |\n| Setting Parameters | `https://orshot.com/docs/orshot-studio/setting-parameters.md` |\n| Image Generation | `https://orshot.com/docs/image-generation.md` |\n| Video Generation | `https://orshot.com/docs/video-generation.md` |\n| PDF Generation | `https://orshot.com/docs/pdf-generation.md` |\n| Social Publishing | `https://orshot.com/docs/publish/introduction.md` |\n| OAuth / Developer Apps | `https://orshot.com/docs/developers.md` |\n| White-Label Embed | `https://orshot.com/docs/orshot-embed/introduction.md` |\n| Integrations | `https://orshot.com/docs/integrations.md` |\n| Dynamic URLs | `https://orshot.com/docs/integrations/dynamic-urls.md` |\n| Webhooks | `https://orshot.com/docs/integrations/webhooks.md` |\n| Error Reference | `https://orshot.com/docs/error-reference.md` |\n\nWhen a user asks about a specific topic, fetch the relevant `.md` URL for the latest details.\n\n## Getting Started\n\n### Authentication\n\nAll API requests require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer <ORSHOT_API_KEY>\n```\n\n[Get your API key](https://orshot.com/docs/quick-start/get-api-key) from **Workspace Settings → API Keys** in the Orshot dashboard.\n\n### Remote MCP Server\n\nOrshot runs a hosted MCP server so agents (Claude, Cursor, Codex, Windsurf, ChatGPT, etc.) can use Orshot as tools — no local install.\n\n- **URL:** `https://mcp.orshot.com/mcp` (transport: `streamable-http`)\n- **Auth:** OAuth 2.0 (the client walks you through it), or send an Orshot API key as a Bearer token\n- **Claude Code:** `claude mcp add --transport http orshot https://mcp.orshot.com/mcp`\n- **Cursor / Windsurf / VS Code / ChatGPT / Codex:** add the URL as a remote/HTTP MCP server\n\nIt exposes tools for rendering, studio + library templates, brand assets, workflows, social accounts, and workspace/logs. Full discovery doc: `https://orshot.com/.well-known/mcp.json`. Setup guide: `https://orshot.com/docs/integrations/mcp-server.md`.\n\n### SDKs\n\n#### Node.js\n\n```bash\nnpm install orshot\n```\n\n```js\nimport { Orshot } from \"orshot\";\nconst orshot = new Orshot(\"<ORSHOT_API_KEY>\");\n\n// Render from template\nconst response = await orshot.renderFromTemplate({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello World\" },\n  responseType: \"base64\", // \"base64\" | \"url\" | \"binary\"\n  responseFormat: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\"\n});\n\n// Generate signed URL\nconst signedUrl = await orshot.generateSignedUrl({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello\" },\n  expiresAt: 1744276943,\n  renderType: \"images\",\n  responseFormat: \"png\",\n});\n```\n\n#### Python\n\n```bash\npip install orshot\n```\n\n```python\nimport orshot\nos = orshot.Orshot('<ORSHOT_API_KEY>')\n\nresponse = os.render_from_template({\n  'template_id': 'open-graph-image-1',\n  'modifications': {'title': 'Hello World'},\n  'response_type': 'base64',\n  'response_format': 'png'\n})\n```\n\n#### Other SDKs\n\n- **PHP:** `composer require rishimohan/orshot`\n- **Ruby:** `gem install orshot`\n\n## Common Gotchas (read before rendering)\n\nThe mistakes that most often make Orshot calls fail or return the wrong output:\n\n1. **Modifications key by `parameterId`, not layer name.** `modifications: { \"headline\": \"...\" }` targets the element whose `parameterId` is `headline`. If a value is ignored, the key is wrong — fetch the template's modifications (`GET /v1/studio/templates/:id`) to see the real keys instead of guessing.\n2. **Multi-page templates need a `pageN@` prefix.** Use `\"page1@title\"`, `\"page2@title\"`. A bare `\"title\"` only affects page 1.\n3. **Studio `templateId` is an integer; utility `templateId` is a string.** `/v1/studio/render` takes an integer ID; `/v1/generate/:renderType` takes a string slug like `\"website-screenshot\"`.\n4. **Video output does NOT require video elements.** `format: \"mp4\"`/`\"webm\"`/`\"gif\"` renders any template — motion can come from video elements, page animations (enter/exit/ambient transitions), page-to-page transitions, or generated narration; a fully static page renders as a still clip of its `videoDuration`. Long video renders should use async render jobs (`response.mode: \"async\"`) so they don't hit client timeouts.\n5. **Image/video URLs in modifications must be publicly reachable.** The renderer fetches them server-side, so `localhost`, expired signed URLs, or auth-gated URLs won't load.\n6. **Style overrides use dot notation on the parameterId** — `\"title.fontSize\": \"48px\"`, not a nested object.\n7. **Response shape differs by page count:** single page → `data` is an object (read `data.content`); multi-page/carousel → `data` is an array of `{ page, content }`. Handle both.\n8. **`base64` and `binary` response types don't combine** — `binary` returns one raw file stream.\n9. **Credits vs AI Credits are separate** — normal renders spend credits; `.prompt` (AI) modifications additionally spend AI Credits.\n10. **On `429`, back off** using the `Retry-After` response header.\n\nWhen unsure about a template's parameters, always fetch its modifications first.\n\n## Template Architecture\n\nThis section describes the complete structure of an Orshot template for MCP tools and AI agents.\n\n### Template Structure\n\nAn Orshot template consists of **pages**, each containing a **canvas** and **elements**.\n\n```\nTemplate\n├── id: number | string\n├── name: string\n├── description: string\n├── width: number\n├── height: number\n├── pages_data: Array\n    └── Page\n        ├── id: string (UUID)\n        ├── name: string\n        ├── canvas: CanvasConfig\n            ├── width: number\n            ├── height: number\n            ├── backgroundColor: string\n            ├── backgroundImage: string\n        ├── elements: Element[]\n        ├── modifications: Modification[] (API parameters)\n            ├── id: string\n            ├── type: string\n            ├── element: Element\n            ├── description: string\n        └── thumbnail_url: string | null\n```\n\n### Canvas Configuration\n\n| Property          | Type   | Default         | Description                               |\n| ----------------- | ------ | --------------- | ----------------------------------------- |\n| `width`           | number | 800             | Canvas width in pixels (max: 5000)        |\n| `height`          | number | 800             | Canvas height in pixels (max: 5000)       |\n| `backgroundColor` | string | \"#ffffff\"       | Background color (hex, rgba, or gradient) |\n| `backgroundImage` | string | \"\"              | URL to background image                   |\n| `borderWidth`     | number | 0               | Border width in pixels                    |\n| `borderColor`     | string | \"rgba(0,0,0,1)\" | Border color                              |\n| `borderStyle`     | string | \"solid\"         | Border style (solid, dashed, etc)         |\n\n### Canvas Size Presets\n\n| Name                 | Dimensions | Use Case                        |\n| -------------------- | ---------- | ------------------------------- |\n| Square               | 1080×1080  | Instagram posts, general social |\n| Instagram Story      | 1080×1920  | Stories, Reels, TikTok          |\n| Slide/Presentation   | 1920×1080  | Presentations, slides           |\n| YouTube Thumbnail    | 1280×720   | Video thumbnails                |\n| Twitter Post         | 1600×900   | X/Twitter posts                 |\n| Open Graph           | 1200×630   | Link previews, Facebook         |\n| Pinterest Pin        | 1000×1500  | Pinterest                       |\n| A4 Document          | 2480×3508  | Print documents                 |\n| App Store Screenshot | 1290×2796  | iOS app screenshots             |\n\n### Universal Element Properties\n\nAll elements share these base properties:\n\n| Property            | Type    | Description                                  |\n| ------------------- | ------- | -------------------------------------------- |\n| `id`                | string  | Unique identifier (UUID)                     |\n| `name`              | string  | Display name in layer list (was `layerName`) |\n| `type`              | string  | \"text\", \"image\", \"shape\", \"video\"            |\n| `position`          | object  | `{ x: number, y: number }` from top-left     |\n| `dimensions`        | object  | `{ width: number, height: number }`          |\n| `rotation`          | number  | Rotation in degrees (0-360)                  |\n| `zIndex`            | number  | Layer order (higher = on top)                |\n| `aspectRatioLocked` | boolean | Lock aspect ratio during resize              |\n| `isHidden`          | boolean | Hide element from render                     |\n| `skewX`             | number  | Horizontal skew angle                        |\n| `skewY`             | number  | Vertical skew angle                          |\n\n### Text Element\n\n**Content Types:**\n\n- **Plain text**: `\"Hello World\"` - Standard text string\n- **Multi-line**: Use `\\n` for line breaks: `\"Line 1\\nLine 2\"`\n- **Dynamic via API**: Use `.prompt` modifier for AI-generated text\n\n```javascript\n{\n  type: \"text\",\n  content: string,          // Plain text string\n  layerName: string,        // Display name\n  zIndex: number,\n  rotation: number,\n  position: { x: number, y: number },\n  dimensions: { width: number, height: number },\n  style: {\n    // Typography\n    fontFamily: string,     // e.g., \"Inter\", \"Prata\", \"SF Pro\"\n    fontSize: string,       // e.g., \"48px\"\n    fontWeight: string | number, // \"400\", \"700\", 700\n    fontStyle: string,      // \"normal\", \"italic\"\n    lineHeight: number,     // e.g., 1.2\n    letterSpacing: string,  // e.g., \"0px\", \"2px\"\n\n    // Appearance\n    fill: string,           // Color or gradient\n    color: string,          // Hex or rgba\n    opacity: number,        // 0-1\n    stroke: string,         // Stroke color\n    strokeWidth: string,    // e.g., \"0px\"\n\n    // Alignment & Layout\n    textAlign: string,      // \"left\", \"center\", \"right\"\n    verticalAlign: string,  // \"flex-start\", \"center\", \"flex-end\"\n    textTransform: string,  // \"none\", \"uppercase\"\n    textDecoration: string, // \"none\", \"underline\"\n    textMode: string,       // \"overflow\", \"fit\"\n    paddingX: string,\n    paddingY: string,\n\n    // Borders & Backgrounds\n    borderColor: string,\n    borderWidth: string,\n    borderRadius: string,\n    textBackgroundColor: string,\n    textBackgroundRadius: string,\n    textStrokeColor: string,\n    textStrokeWidth: string,\n\n    // Effects\n    minFontSize: string,    // For \"fit\" mode\n    filter: string,         // e.g., \"blur(0px)\"\n    mixBlendMode: string,   // \"normal\", \"multiply\", etc.\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  // Parameterization\n  parameterizable: boolean,\n  parameterId: string,\n  parameterType: \"text\"\n}\n```\n\n**Gradient text:**\n\n```javascript\ncolor: \"linear-gradient(90deg, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Image Element\n\n**Content Types:**\n\n- **URL** (recommended): `\"https://example.com/image.png\"` - Best for dynamic content\n- **Base64**: `\"data:image/png;base64,iVBORw0KGgo...\"` - For embedded images\n- **Binary**: Raw binary data (API upload only)\n\n```javascript\n{\n  type: \"image\",\n  content: string,          // URL (preferred), base64, or binary\n  isSvg: boolean,\n  layerName: string,\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string, // \"center\", \"top left\"\n\n    // Appearance\n    opacity: number,\n    fill: string,           // Background fill\n    stroke: string,         // Border stroke\n\n    // Borders\n    borderRadius: string,   // \"0px\", \"12px\", \"50%\"\n    borderWidth: string,\n    borderColor: string,\n\n    // Effects\n    filter: string,         // \"blur(2px)\", \"grayscale(100%)\"\n    mixBlendMode: string,\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string,\n\n    svgColor: string        // Recolor monochrome SVGs\n  },\n  parameterType: \"imageUrl\"\n}\n```\n\n### Shape Element\n\n```javascript\n{\n  type: \"shape\",\n  shapeType: string,        // \"rectangle\", \"circle\", \"arrow\"\n  layerName: string,\n  style: {\n    // Fill & Stroke\n    fill: string,           // Color or gradient\n    stroke: string,\n    strokeWidth: string,    // e.g. \"0px\"\n\n    // Dimensions\n    borderRadius: string,   // Rectangle only\n    borderWidth: string,\n    borderColor: string,\n    borderStyle: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Shadows\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  parameterType: \"fill\"\n}\n```\n\n**Gradient fills:**\n\n```javascript\nfill: \"linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%)\";\nfill: \"radial-gradient(circle at center, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Video Element\n\n**Content Types:**\n\n- **URL** (required): `\"https://example.com/video.mp4\"` - Must be a publicly accessible URL\n- Supported formats: MP4, WebM, MOV\n- For best results, use MP4 with H.264 codec\n\n```javascript\n{\n  type: \"video\",\n  content: string,          // Video URL (must be publicly accessible)\n  videoOptions: {\n    loop: boolean,\n    muted: boolean,\n    trim_start_time: string,\n    trim_end_time: string,\n    duration: number | null\n  },\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Borders & Shadows\n    borderRadius: string,\n    borderWidth: string,\n    borderColor: string,\n    elementBoxShadowX: string,\n    elementBoxShadowY: string,\n    elementBoxShadowBlur: string,\n    elementBoxShadowColor: string\n  },\n  parameterType: \"videoUrl\"\n}\n```\n\n### Parameterization Best Practices\n\nWhen creating or updating templates, **always ensure all text, image, and video elements are parameterizable** with unique IDs. This enables dynamic content replacement via the API.\n\n#### Required Setup\n\nEvery dynamic element MUST have:\n\n```javascript\n{\n  parameterizable: true,\n  parameterId: \"unique_id\",  // Unique across template, lowercase with underscores\n  parameterType: \"text\" | \"imageUrl\" | \"videoUrl\"\n}\n```\n\n#### Naming Conventions\n\n| Element Type | parameterId Examples                        | parameterType |\n| ------------ | ------------------------------------------- | ------------- |\n| Text         | `headline`, `subtitle`, `cta_text`, `price` | `\"text\"`      |\n| Image        | `product_image`, `logo`, `background_image` | `\"imageUrl\"`  |\n| Video        | `hero_video`, `background_video`            | `\"videoUrl\"`  |\n\n#### Best Practices\n\n1. **Use descriptive IDs:** `product_title` not `text1`\n2. **Be consistent:** Use snake_case across all templates\n3. **Unique per template:** No duplicate parameterIds on same page\n4. **Group logically:** Related elements share naming prefix (e.g., `card_title`, `card_image`)\n\n#### Validation Checklist\n\nBefore finalizing any template update:\n- [ ] All text elements have `parameterizable: true` and unique `parameterId`\n- [ ] All image elements have `parameterizable: true` and unique `parameterId`\n- [ ] All video elements have `parameterizable: true` and unique `parameterId`\n- [ ] No duplicate parameterIds exist on the same page\n- [ ] parameterIds are descriptive and follow snake_case convention\n\n### Design Best Practices\n\n#### Typography Guidelines\n- **Font limit:** Use 2-3 fonts maximum per template\n- **Hierarchy:** Headings should be 1.5-2x larger than body text\n- **Minimum size:** 24px for social media readability\n- **Weights:** Headings 600-900 (bold), Body 400-500 (regular)\n\n**Popular font pairings:**\n- `Prata` + `Inter`\n- `Instrument Serif` + `DM Sans`\n- `Playfair Display` + `Lato`\n- `Montserrat` + `Open Sans`\n\n**Platform-specific fonts:**\n- iOS: `SF Pro Display`, `SF Pro Text`\n- Android: `Google Sans`, `Roboto`\n\n#### Color Guidelines\n**Luxury/Gold palette:**\n- Gold: `#D4AF37`\n- Dark gold: `#B8860B`\n- Light gold: `#F5E7A3`\n\n**iOS system colors:**\n- Blue: `#007AFF`\n- Gray: `#8E8E93`\n- Background: `#F5F5F7`\n\n**Professional dark:**\n- Navy: `#0F172A`\n- Slate: `#1E293B`, `#334155`\n- Muted: `#64748B`, `#94A3B8`\n\n**Best practices:**\n- Ensure 4.5:1 minimum contrast for text readability\n- Use gradients sparingly for premium effects\n- Consistent color palette (3-5 colors max)\n\n#### Layout Guidelines\n- **Edge padding:** 40-60px from canvas edges\n- **Element spacing:** 20-40px between elements\n- **Alignment:** Center for formal, left for modern\n- **Visual flow:** Guide eye with size, color, position\n\n**zIndex ordering:**\n- Background images/colors: 1\n- Overlay shapes: 2-3\n- Text elements: 4-6\n- Interactive elements: 7+\n\n#### Common Operations (Design Automation)\n\n**Add text element:**\n```javascript\naddElement(\"text\", {\n  content: \"Hello World\",\n  fontFamily: \"Inter\",\n  fontSize: 48,\n  fontWeight: \"700\",\n  color: \"#FFFFFF\",\n  x: 100,\n  y: 100,\n  width: 400,\n  height: 60,\n});\n```\n\n**Add shape backdrop:**\n```javascript\naddElement(\"rectangle\", {\n  fill: \"rgba(0,0,0,0.5)\",\n  width: 1080,\n  height: 200,\n  x: 0,\n  y: 800,\n  borderRadius: \"0px\",\n});\n```\n\n**Batch update multiple elements:**\n```javascript\nbatchUpdate([\n  {\n    elementId: \"heading\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { fontSize: 72 } },\n  },\n  {\n    elementId: \"subtitle\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { color: \"#94A3B8\" } },\n  },\n  { type: \"ORSHOT_UPDATE_CANVAS\", updates: { backgroundColor: \"#0F172A\" } },\n]);\n```\n\n## API Reference\n\n### 1. Render from Studio Template\n\nGenerate images/PDFs/videos from templates designed in Orshot Studio.\n\n**POST** `https://api.orshot.com/v1/studio/render`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123, // Integer - your studio template ID\n    modifications: {\n      title: \"Hello World\",\n      imageUrl: \"https://example.com/photo.jpg\",\n      canvasBackgroundColor: \"#eff2fa\",\n    },\n    response: {\n      type: \"base64\", // \"base64\" | \"url\" | \"binary\"\n      format: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\" | \"mp4\" | \"webm\" | \"gif\"\n      scale: 1, // 1 = original size, 2 = double\n      includePages: [1, 3], // optional – only for multi-page templates\n      fileName: \"my-render\", // optional – custom filename (without extension)\n    },\n    pdfOptions: {\n      // optional – only when format is \"pdf\"\n      margin: \"20px\",\n      rangeFrom: 1,\n      rangeTo: 2,\n      colorMode: \"rgb\", // \"rgb\" or \"cmyk\"\n      dpi: 300,\n    },\n  }),\n});\n```\n\n**Response (single page):**\n```json\n{\n  \"data\": {\n    \"content\": \"data:image/png;base64,iVBORw0.....\",\n    \"format\": \"png\",\n    \"type\": \"base64\",\n    \"responseTime\": 325.22\n  }\n}\n```\n\n**Response (multi-page/carousel):**\n```json\n{\n  \"data\": [\n    { \"page\": 1, \"content\": \"https://storage.orshot.com/.../image1.png\" },\n    { \"page\": 2, \"content\": \"https://storage.orshot.com/.../image2.png\" }\n  ],\n  \"format\": \"png\",\n  \"type\": \"url\",\n  \"responseTime\": 3166.01,\n  \"totalPages\": 2,\n  \"renderedPages\": 2\n}\n```\n\n### 2. Render from Utility Template\n\nGenerate renders from Orshot's pre-built utility templates (e.g., website screenshots, tweet images).\n\n**POST** `https://api.orshot.com/v1/generate/{renderType}`\n\n- `renderType`: `images` or `pdfs`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/generate/images\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\", // String ID for utility templates\n    response: {\n      format: \"png\",\n      type: \"base64\",\n    },\n    modifications: {\n      websiteUrl: \"https://example.com\",\n      fullCapture: false,\n      delay: 500,\n      width: 1200,\n      height: 1000,\n    },\n  }),\n});\n```\n\n### 3. Generate Signed URL\n\nCreate publicly accessible render URLs without exposing your API key.\n\n**POST** `https://api.orshot.com/v1/signed-url/create`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/signed-url/create\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\",\n    expiresAt: 1744550160505, // UNIX timestamp, or null for no expiry\n    renderType: \"images\",\n    modifications: {\n      websiteUrl: \"https://example.com\",\n    },\n  }),\n});\n```\n\n### 4. List Studio Templates\n\n**GET** `https://api.orshot.com/v1/studio/templates/all?page=1&limit=10`\n\nResponse includes `data` array of templates and `pagination` object with `page`, `limit`, `total`, `totalPages`.\n\n### 5. Get Studio Template\n\n**GET** `https://api.orshot.com/v1/studio/templates/:templateId`\n\nReturns the template metadata including available `modifications`.\n\n### 6. Delete Studio Template\n\n**DELETE** `https://api.orshot.com/v1/studio/templates/:templateId`\n\n### 7. Duplicate Studio Template\n\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/duplicate`\n\n### 8. Get Profile & Workspaces\n\n**GET** `https://api.orshot.com/v1/me`\n\nReturns your profile and the workspaces your API key or OAuth token can access, including plan info and credit usage.\n\n### 9. Brand Assets API\n\nBrand assets are grouped by type — **images**, **colors**, **fonts**, **videos**, **audio** — each with its own routes under `/v1/brand-assets/{type}/…`.\n\n#### Images\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/images/get`\n- **Upload:** `POST https://api.orshot.com/v1/brand-assets/images/add` — `multipart/form-data` with a `file` field\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/images/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/images/delete/:id`\n\n#### Colors\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/colors/get`\n- **Add:** `POST https://api.orshot.com/v1/brand-assets/colors/add` — body `{ type: \"hex\" | \"gradient\", value, tags? }`\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/colors/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/colors/delete/:id`\n\n#### Fonts / Videos / Audio\nSame shape — swap the type segment (`fonts`, `videos`, `audio`):\n- **Get:** `GET /v1/brand-assets/{type}/get`\n- **Upload:** `POST /v1/brand-assets/{type}/add` (`multipart/form-data`, `file` field)\n- **Update tags:** `PATCH /v1/brand-assets/{type}/update/:id`\n- **Delete:** `DELETE /v1/brand-assets/{type}/delete/:id`\n\n#### Search\n- **Search assets:** `GET https://api.orshot.com/v1/brand-assets/search`\n\nUse an uploaded asset in a render by passing its hosted URL as an image/video modification value.\n\n### 10. Enterprise API Endpoints\n\nThese endpoints require an Enterprise plan.\n\n#### Create Studio Template\n**POST** `https://api.orshot.com/v1/studio/templates/create`\n\n```js\n{\n  name: \"Product Banner\",        // required, max 255 chars\n  description: \"Banner template\", // optional\n  canvas_width: 1200,             // required, 1-5000\n  canvas_height: 628,             // required, 1-5000\n  pages_data: [...]               // optional - array of page objects with elements\n}\n```\n\n#### Bulk Create Studio Templates\n**POST** `https://api.orshot.com/v1/studio/templates/bulk-create`\nCreate multiple templates at once via CSV or JSON.\n\n#### Update Template\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId`\nUpdate template name and description.\n\n#### Update Template Modifications\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId/update-modifications`\nUpdate text and image content in template layers.\n\n#### Generate Template Variants\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/generate-variants`\nGenerate multiple size variants of a template using AI.\n\n### 11. Dynamic URLs\n\nGenerate images directly from URL parameters:\n\n```\nhttps://api.orshot.com/v1/studio/dynamic-url/my-image?title=Hello%20World&title.fontSize=48px&title.color=%23ff0000\n```\nURL-encode special characters (e.g., `#` → `%23`).\n\n### 12. Template Folders & Sharing\n\nOrganize studio templates into folders and share them:\n\n- **List folders:** `GET https://api.orshot.com/v1/studio/folders`\n- **Create folder:** `POST https://api.orshot.com/v1/studio/folders`\n- **Update folder:** `PATCH https://api.orshot.com/v1/studio/folders/:folderId`\n- **Delete folder:** `DELETE https://api.orshot.com/v1/studio/folders/:folderId`\n- **Move template into folder:** `PATCH https://api.orshot.com/v1/studio/templates/:templateId/folder`\n- **Get template sharing:** `GET https://api.orshot.com/v1/studio/templates/:templateId/share`\n- **Update template sharing:** `POST https://api.orshot.com/v1/studio/templates/:templateId/share`\n\n### 13. Workflows API\n\nWorkflows automate multi-step pipelines (trigger → fetch data → render → publish/deliver). Available to first-party clients (like the Orshot MCP server) and Enterprise workspaces.\n\n- **List workflows:** `GET https://api.orshot.com/v1/workflows`\n- **Create workflow:** `POST https://api.orshot.com/v1/workflows`\n- **Get workflow:** `GET https://api.orshot.com/v1/workflows/:id`\n- **Update workflow:** `PATCH https://api.orshot.com/v1/workflows/:id`\n- **Delete workflow:** `DELETE https://api.orshot.com/v1/workflows/:id`\n- **Run workflow:** `POST https://api.orshot.com/v1/workflows/:id/run`\n- **List runs:** `GET https://api.orshot.com/v1/workflows/:id/runs`\n- **Get a run:** `GET https://api.orshot.com/v1/workflows/:id/runs/:runId`\n- **Validate a workflow:** `POST https://api.orshot.com/v1/workflows/validate`\n- **List available nodes:** `GET https://api.orshot.com/v1/workflows/nodes`\n- **Get/update sharing:** `GET` / `POST https://api.orshot.com/v1/workflows/:id/share`\n\n## Render Configuration\n\n### Dynamic Parameters\n\nOverride template styles, content, and behavior at render time using dot notation.\n\n#### Style Parameters\n\nFormat: `parameterId.property`\n\n```json\n{\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"title.fontSize\": \"48px\",\n    \"title.color\": \"#ff0000\",\n    \"title.fontFamily\": \"Roboto\",\n    \"title.textAlign\": \"center\",\n    \"logo.borderRadius\": \"50%\",\n    \"logo.objectFit\": \"cover\"\n  }\n}\n```\n\n**Text properties:** `fontSize`, `fontWeight`, `fontStyle`, `fontFamily`, `lineHeight`, `letterSpacing`, `textAlign`, `verticalAlign`, `textDecoration`, `textTransform`, `color`, `backgroundColor`, `backgroundRadius`, `textStrokeWidth`, `textStrokeColor`, `opacity`, `filter`, `dropShadowX/Y/Blur/Color`\n\n**Image properties:** `objectFit`, `objectPosition`, `borderRadius`, `borderWidth`, `borderColor`, `boxShadowX/Y/Blur/Color`, `opacity`, `filter`\n\n**Shape properties:** `fill`, `stroke`, `strokeWidth`, `borderRadius`, `opacity`\n\n**Position/Size (all elements):** `x`, `y`, `width`, `height`\n\nProperty names are **case-insensitive**.\n\n#### Multi-Page Templates\n\nPrefix modifications with page number:\n```json\n{\n  \"modifications\": {\n    \"page1@title\": \"Page 1 Title\",\n    \"page2@title\": \"Page 2 Title\",\n    \"page1@title.fontSize\": \"48px\"\n  }\n}\n```\n\n#### AI Content Generation (.prompt)\n\nGenerate text or images using AI:\n```json\n{\n  \"modifications\": {\n    \"headline.prompt\": \"Write a catchy headline about coffee\",\n    \"background.prompt\": \"A serene mountain landscape at sunset\"\n  }\n}\n```\n- `.prompt` on a text element generates copy; on an image element it generates imagery. The underlying AI models are managed by Orshot and may change over time — don't hardcode a specific model. AI modifications consume AI Credits.\n\n#### Interactive Links (.href)\n\nAdd clickable links in PDF outputs:\n```json\n{\n  \"modifications\": {\n    \"cta_button.href\": \"https://example.com/signup\",\n    \"logo.href\": \"https://company.com\"\n  },\n  \"response\": { \"format\": \"pdf\" }\n}\n```\n\n#### Video Parameters\n\nControl video elements dynamically:\n```json\n{\n  \"modifications\": {\n    \"bgVideo\": \"https://example.com/video.mp4\",\n    \"bgVideo.trimStart\": 5,\n    \"bgVideo.trimEnd\": 15,\n    \"bgVideo.muted\": false,\n    \"bgVideo.loop\": true\n  },\n  \"response\": { \"format\": \"mp4\" }\n}\n```\n\n### Video Render Example\n\nRender templates with video elements as MP4, WebM, or GIF:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: {\n      videoElement: \"https://example.com/custom-video.mp4\",\n      \"videoElement.trimStart\": 0,\n      \"videoElement.trimEnd\": 10,\n      \"videoElement.muted\": false,\n      \"videoElement.loop\": true,\n    },\n    videoOptions: {\n      trimStart: 0,\n      trimEnd: 20,\n      muted: true,\n      loop: true,\n    },\n    response: {\n      type: \"url\",\n      format: \"mp4\",\n    },\n  }),\n});\n```\n\n### Render-level `videoOptions` (top level, sibling of `modifications`)\n\nControls the whole video render, not one element:\n\n```json\n{\n  \"templateId\": 123,\n  \"modifications\": { \"page1@headline\": \"Launch day\" },\n  \"videoOptions\": {\n    \"duration\": 12,\n    \"fps\": 30,\n    \"muted\": false,\n    \"combinePages\": true,\n    \"pageTransition\": \"fade\",\n    \"pageTransitionDuration\": 0.5,\n    \"audioSource\": \"https://example.com/track.mp3\",\n    \"subtitleSource\": \"https://example.com/voiceover.mp3\",\n    \"captionPreset\": \"karaoke\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"mp4\" }\n}\n```\n\n- `duration`, `fps` (≤30), `quality`, `muted`, `trimStart`+`trimEnd` (must be paired).\n- `combinePages: true` merges a multi-page template into ONE video, with `pageTransition` (42 types — fades, slides, wipes, zooms, blurs and more) and `pageTransitionDuration` (0–2s, default 0.5) between scenes. A string `audioSource` then lays one continuous soundtrack across the combined video — the recipe for multi-scene videos.\n- `audioSource` swaps an existing URL audio track at render time while keeping its saved trim/fades/volume/offset/loop. Generated narration changes through `tts.parameterId` instead (see Audio below).\n- Captions: `subtitleSource` (a media URL to auto-transcribe, or a `.srt`/`.vtt` used as-is), `captionPreset` (`pill` | `minimal` | `karaoke` | `bold` | `outline` | `neon`), plus fine-grained `subtitleMode` (`phrase`|`word-by-word`), `subtitleWordReveal` (`appear`|`highlight`|`align`), `subtitleSplit` (`word`|`letter`), `subtitleMaxWordsPerLine` or `subtitleMaxCharsPerLine`, and `subtitle*` styling keys. Prefer authored `page.subtitle` with source `\"auto-audio\"` so captions word-sync automatically.\n\n### Async render jobs (long videos — use this from automations)\n\nA synchronous render holds the HTTP connection for the full render time; video routinely outlives client timeouts (Zapier ~30s, many MCP clients 120s). Add `response.mode: \"async\"`:\n\n```\nPOST /v1/studio/render  { ..., \"response\": { \"mode\": \"async\", \"format\": \"mp4\" } }\n  → 202 with a job object in ~100ms\nGET  /v1/studio/render-jobs/:id          → poll the job (queued → processing → succeeded/failed/canceled)\nGET  /v1/studio/render-jobs?status=&limit=  → newest-first list\nPOST /v1/studio/render-jobs/:id/cancel   → cancel while not terminal\n```\n\nThe job's `result` is byte-identical to the sync response. Pass a `webhook_url` to get the settled job POSTed to you instead of polling. Jobs time out at 15 minutes.\n\n### Page animations & ambient motion (video templates)\n\nElements animate via a page-level `transitions` object on the element — no video file needed:\n\n```json\n{\n  \"transitions\": {\n    \"enter\": { \"type\": \"slideInUp\", \"duration\": 0.5, \"easing\": \"ease-out\" },\n    \"showAt\": 0.4,\n    \"ambient\": { \"type\": \"kenBurns\", \"customValues\": { \"zoom\": 1.12, \"panX\": 6 } },\n    \"exit\": { \"type\": \"fadeOut\", \"duration\": 0.3 },\n    \"hideAt\": 3.6\n  }\n}\n```\n\n- Three phases: `enter` → `ambient` (plays the element's whole visible life) → `exit`. `showAt`/`hideAt` are siblings of `enter`/`exit`, never nested inside them (the #1 authoring bug).\n- Ambient types: `kenBurns` (runs once, stretched across visibility — give every full-bleed photo one), plus looping `pulse`, `float`, `drift`, `wiggle`, `shake`, `slowRotate`, `breathe`. `shake`/`slowRotate` need `easing: \"linear\"` or the loop seam shows.\n- All timing is tunable per render via dot-notation modifications: `\"photo.showAt\": 2.1`, `\"photo.enterType\": \"scaleIn\"`, `\"photo.ambientType\": \"kenBurns\"`, etc.\n\n### Audio: soundtracks & AI voiceover (video templates)\n\nEach page carries `audioTracks` (max 10, page-level, mixed into every mp4/webm render):\n\n- **Existing audio**: `{ \"url\": \"https://...\", \"trimStart\": 33.2, \"trimEnd\": 44.1, \"fadeOut\": 1, \"volume\": 1, \"loop\": false, \"offset\": 0 }` — trim to the song's best part; `videoOptions.audioSource` swaps the file at render keeping these settings.\n- **AI voiceover**: `{ \"tts\": { \"text\": \"words to speak\", \"voiceId\": \"...\", \"modelId\": \"flash\", \"parameterId\": \"voiceover\" } }` instead of `url`. Synthesized when the template is saved (metered voice minutes; identical scripts are cached and free on re-render). Narration length drives the page length, and `page.subtitle` with source `\"auto-audio\"` gives word-synced captions with no transcription step.\n- **Generated ambient/SFX**: `{ \"sfx\": { \"prompt\": \"warm modern pulse, instrumental, seamless loop\", \"seconds\": 12 }, \"volume\": 0.12, \"loop\": true }`.\n- `tts.parameterId` makes the SPOKEN TEXT a render parameter: `modifications: { \"voiceover\": \"new words\" }` (multi-page: `\"page1@voiceover\"`) re-voices the video per render; `\"voiceover.voiceId\"` / `\"voiceover.modelId\"` tune the voice.\n- Budget narration at ~2 words/second plus 1 second of headroom for the page duration.\n- Via MCP, call `orshot_get_audio_authoring_options` first — it returns valid voice ids, models and the workspace's voice allowance in one read-only call.\n\n### Smart Resize (one design → any size)\n\nRender a template at a **different canvas size without redesigning it** — the layout is deterministically re-solved to fit (elements re-anchor, backgrounds stretch, groups move together). Set these on the `response` object:\n\n- **`size`** — *replace* the render size. A preset slug (`\"instagram-story\"`, `\"og-image\"`, `\"youtube-thumbnail\"`, …), a `\"WIDTHxHEIGHT\"` string (`\"1080x1920\"`), or use `width` + `height` (10–5000px each).\n- **`extraSizes`** — *add* the same design at extra sizes in one call. An array (`[\"1080x1920\", \"1080x1080\"]`) or a named object (`{ story: \"1080x1920\", square: \"1080x1080\" }`). Each output gains a nested `extraSizes` array of `{ size, width, height, content }`.\n\n```json\n{\n  \"templateId\": 123,\n  \"modifications\": { \"title\": \"Launch Day\" },\n  \"response\": {\n    \"type\": \"url\",\n    \"format\": \"png\",\n    \"size\": \"1200x630\",\n    \"extraSizes\": [\"1080x1920\", \"1080x1080\"]\n  }\n}\n```\n\nImage formats only (`png`/`jpg`/`webp`/`avif`), up to 50 extra outputs per call. Each extra output is billed like a page (1 credit). Saved sizes from the Studio Smart Resize panel reproduce their approved preview exactly.\n\n### Response Types\n\n| Type     | Description                                     |\n| -------- | ----------------------------------------------- |\n| `url`    | Returns a hosted URL to the rendered file       |\n| `base64` | Returns base64-encoded content as a string      |\n| `binary` | Returns binary file content for custom handling |\n\n### Response Formats\n\n| Format | Type  | Notes                                      |\n| ------ | ----- | ------------------------------------------ |\n| `png`  | Image | Best quality, larger size                  |\n| `webp` | Image | Smaller size, good quality                 |\n| `jpg`  | Image | Compressed, no transparency                |\n| `avif` | Image | Smallest size, modern browsers             |\n| `pdf`  | Doc   | Supports multi-page, clickable links, CMYK |\n| `mp4`  | Video | H.264, requires video elements in template |\n| `webm` | Video | VP9, web-optimized                         |\n| `mov`  | Video | QuickTime container                        |\n| `mkv`  | Video | Matroska container                         |\n| `gif`  | Video | Animated, no audio support                 |\n\n### Render Usage (credits)\n\nUsage is measured in **credits**. 1 credit = 1 image, 1 PDF page, or 1 second of video.\n\n| Output               | Cost                     |\n| -------------------- | ------------------------ |\n| Image (PNG/JPG/WebP/AVIF) | 1 credit per image  |\n| PDF                  | 1 credit per page        |\n| Video (MP4/WebM/MOV/MKV/GIF) | 1 credit per second |\n\nMulti-page templates and Smart Resize extra sizes: each output page/size counts as its own credit. AI modifications (`.prompt`) additionally consume AI Credits.\n\n## Common Recipes\n\nEnd-to-end patterns for the jobs people most often automate with Orshot.\n\n### OG image on every deploy\nRender a studio template to a stable hosted URL and drop it into your `<meta>` tags.\n```js\nconst { data } = await fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: post.title, author: post.author },\n    response: { type: \"url\", format: \"png\", size: \"og-image\" },\n  }),\n}).then((r) => r.json());\n// <meta property=\"og:image\" content={data.content} />\n```\n\n### Bulk-generate from a CSV (certificates, badges, invoices)\nLoop rows and render one PDF each. Describe image layers with `.alt` for accessible PDFs.\n```js\nfor (const row of rows) {\n  await fetch(\"https://api.orshot.com/v1/studio/render\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n    body: JSON.stringify({\n      templateId: 456,\n      modifications: { name: row.name, course: row.course, date: row.date },\n      response: { type: \"url\", format: \"pdf\" },\n      pdfOptions: { title: `${row.name} — Certificate` },\n    }),\n  });\n}\n```\n\n### Render and auto-post to social in one call\nAdd a `publish` object (see Social Publishing) — no second request.\n```js\nbody: JSON.stringify({\n  templateId: 123,\n  modifications: { title: \"Launch day!\" },\n  response: { type: \"url\", format: \"png\" },\n  publish: { accounts: [1, 2], content: \"We just shipped 🚀\" },\n});\n```\n\n### One design, every social size (Smart Resize)\n```js\nresponse: { type: \"url\", format: \"png\", extraSizes: [\"1080x1920\", \"1080x1080\", \"1200x630\"] }\n// each output gains a nested extraSizes[] of { size, width, height, content }\n```\n\n### Automate a recurring pipeline (Workflows)\nCreate a workflow (trigger → fetch data → render → publish/deliver) with `POST /v1/workflows`, then trigger it with `POST /v1/workflows/:id/run`. Inspect results via `GET /v1/workflows/:id/runs`. Best driven through the MCP server or the Workflows API on Enterprise/first-party clients.\n\nSee **Setting Up Recurring Automation** below for the full playbook, including how\nto wire it into an n8n/Make/Zapier setup the user already runs.\n\n## Setting Up Recurring Automation\n\nA one-off render is a demo. The value shows up when it runs unattended. Pick the\nroad that matches where the user ALREADY works, not the one easiest to describe.\n\n### Choose the road first\n\nCheck what they already run before recommending anything. `GET /v1/workspace/logs?limit=100`\nreturns a `source` on every row, which answers it factually:\n\n| `source` in their logs | They already run | Lead with |\n| --- | --- | --- |\n| `n8n-integration` | n8n | Add a node to the n8n workflow they have |\n| `orshot-make` | Make | Add a module to their scenario |\n| `zapier-integration` | Zapier | Add an action to their Zap |\n| `orshot-pipedream` | Pipedream | Add a step |\n| `orshot-*-sdk`, `api`, `cli` | Their own code | Write the call into their repo |\n| only `playground` / `orshot-mcp-server` | Nothing yet | Offer Orshot Workflows |\n\nNever recommend a second orchestrator to someone who already has one. A user with a\nlive n8n setup wants this template added to it, not a new tool to learn.\n\nIf you have browser control or repo access, offer to DO the setup rather than\ndescribe it. Whichever road you take, **run it once and show the resulting image\nURL** before calling it done.\n\n### Road A — Orshot Workflows (no orchestrator yet)\n\nOrshot runs the whole loop: trigger, render, deliver. Over MCP:\n\n1. `orshot_suggest_workflows` with the `templateId` — returns automations that\n   fit this template, with draft-ready steps\n2. `orshot_list_workflow_nodes` — use the EXACT node keys it returns. Invented\n   keys (e.g. `render_studio_template`, `slack_send`) fail as `unknown node`;\n   the real keys are `render`, `slack`, and so on\n3. `orshot_list_connected_integrations` — see what is already connected\n4. `orshot_get_workflow_connection_data` — resolve \"my content sheet\" into a\n   concrete id, and confirm the name back to the user\n5. `orshot_validate_workflow` — read `errors`, `warnings` AND `gated`. A `gated`\n   entry means the step is not included in the workspace's current plan, not a\n   config problem: the draft still saves, only activation is blocked, so tell\n   the user that instead of editing steps\n6. `orshot_create_workflow` with `status: \"draft\"`, then share the edit link from\n   the result — it opens pre-configured in their dashboard\n7. `orshot_run_workflow` — prove it works, then they activate\n\n**Scaffolding shapes.** If they have no data source ready, use\n`webhook` → `webhook_body` → `render` → `orshot_url`. It is the only\ntrigger→source pair needing no OAuth AND it validates clean with empty configs,\nso it is runnable immediately and they can POST to it from cron, their app, n8n,\nor Make. A bare `schedule` → `render` is NOT runnable — it fails with \"no data\nsource\"; a schedule needs a source step after it.\n\n### Road B — their existing automation platform\n\nEvery platform needs the same three things: endpoint, auth header, modification\nkeys. Get the exact keys first — never guess them:\n\n```\norshot_get_studio_template_modifications   (or GET /v1/studio/templates/:id/modifications)\n```\n\nThen the call to drop in:\n\n```http\nPOST https://api.orshot.com/v1/studio/render\nAuthorization: Bearer <ORSHOT_API_KEY>\nContent-Type: application/json\n\n{\n  \"templateId\": 1234,\n  \"modifications\": { \"headline\": \"{{ row.title }}\", \"hero_image\": \"{{ row.image_url }}\" },\n  \"response\": { \"type\": \"url\", \"format\": \"png\" }\n}\n```\n\nThe response contains the rendered URL — map it to whatever the next step needs.\n\n- **n8n** — HTTP Request node, POST, header auth, body as above. A dedicated\n  Orshot node also exists in the n8n library.\n- **Make** — HTTP \"Make a request\" module, or the Orshot app modules.\n- **Zapier** — Webhooks by Zapier → POST, or the Orshot Zapier app.\n- **Pipedream** — HTTP step; Orshot components are published.\n- **Own code / cron** — any HTTP client. Use `response.type: \"url\"` in production\n  so you are not moving base64 around, and keep the API key in an environment\n  variable, never inline.\n\nRecurring triggers that work well: a new spreadsheet/Airtable/Notion row, a\nschedule, an inbound webhook, or a CMS publish event.\n\n### Verify before you call it done\n\n1. Trigger one real run\n2. `orshot_list_workspace_logs` (or `GET /v1/workspace/logs`) — the new render\n   appears with the `source` of whatever fired it, proving which system made it\n3. Show the user the image URL from that run\n\nIf the render 403s, read the body: an expired key, an inactive subscription, and\na plan limit all return 403 with different messages. A plan block is not an auth\nproblem — do not retry it as one.\n\n## Integrations & Helps\n\n### Integrations Service Support\nOrshot connects with:\n- **No-code:** Zapier, Make (Integromat), n8n, Pipedream, Airtable\n- **Storage:** Amazon S3, Cloudflare R2, Google Drive, Dropbox\n- **Notifications:** Slack, Webhooks\n- **Design:** Figma Plugin, Canva Import, Polotno Import\n- **CLI:** `npx orshot-cli` for terminal-based generation\n- **MCP Server:** Use with Claude, Cursor, Windsurf via MCP protocol\n- **Embed:** White-label design editor for your app (React SDK, Vue SDK, iframe)\n\n### Common Error Codes\n\n| Code | Error                          | Fix                                          |\n| ---- | ------------------------------ | -------------------------------------------- |\n| 400  | `templateId missing`           | Add `templateId` to request body             |\n| 400  | `Invalid API Key`              | Generate new key from dashboard              |\n| 403  | `Authorization header missing` | Add `Authorization: Bearer <KEY>` header     |\n| 403  | `Subscription inactive`        | The workspace subscription is not active; check it in the dashboard |\n| 403  | `Template not found`           | Verify template ID belongs to your workspace |\n| 403  | `ai-video-not-on-plan`         | AI video is not included in the workspace's current plan; use stock footage or a Brand Library video instead |\n\n### General Best Practices\n\n1. **Use `url` response type** for production – avoids large base64 payloads\n2. **Use `webp` format** for smaller file sizes with good quality\n3. **Test in Playground** before coding – each template has an interactive playground\n4. **Use style parameters** instead of creating multiple template variants\n5. **Use consistent units** – stick to `px` for sizes\n6. **Multi-page prefix** – always use `page1@paramId` format for carousel templates\n7. **Cache renders** – use `?cache=false` query param to bypass cache when needed\n8. **Handle errors** – check for 403/400 status codes and parse error messages\n\n## Social Publishing\n\nPublish rendered images/videos directly to 13+ social platforms via API. Supports Twitter/X, Instagram, LinkedIn, Pinterest, Facebook, TikTok, YouTube, Threads, Bluesky, Reddit, Telegram, Snapchat, and Google Business.\n\nFor detailed setup: fetch `https://orshot.com/docs/publish/introduction.md`\n\n### Connect Accounts\n\nConnect social accounts in **Workspace Settings → Social Accounts** in the Orshot dashboard. Each connected account gets a numeric ID used in API calls.\n\n### Publish from API\n\nAdd the `publish` object to any render request:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: \"New Post\" },\n    response: { type: \"url\", format: \"png\" },\n    publish: {\n      accounts: [1, 3],          // Social account IDs\n      content: \"Check this out!\", // Caption text (max 5000 chars)\n      isDraft: false,             // true = save as draft\n      schedule: {                 // Optional: schedule for later\n        scheduledFor: \"2026-04-25T14:00:00Z\"\n      },\n      timezone: \"America/New_York\",\n      platformOptions: {          // Per-account overrides (keyed by account ID)\n        \"1\": { firstComment: \"https://example.com\" },     // LinkedIn\n        \"3\": { title: \"Pin Title\", link: \"https://...\" }   // Pinterest\n      }\n    }\n  }),\n});\n```\n\n**Response includes publish status:**\n```json\n{\n  \"data\": { \"content\": \"https://storage.orshot.com/...\" },\n  \"publish\": [\n    { \"platform\": \"twitter\", \"username\": \"acme\", \"status\": \"published\", \"url\": \"https://x.com/...\" },\n    { \"platform\": \"linkedin\", \"username\": \"acme\", \"status\": \"scheduled\" }\n  ]\n}\n```\n\n**Format compatibility:**\n- PDF cannot be published to any platform\n- Video (mp4/webm/gif) cannot be published to Google Business\n- Image (png/jpg/webp) cannot be published to TikTok or YouTube (video-only)\n\n## Developer Apps & OAuth\n\nBuild third-party integrations with Orshot using OAuth 2.0. Register an app, authenticate users, and access their workspaces on their behalf.\n\nFor detailed setup: fetch `https://orshot.com/docs/developers/oauth-overview.md`\n\n### Register an App\n\n1. Go to **Workspace Settings → Developer Apps** in the Orshot dashboard\n2. Create a new app with a name, redirect URI, and the scopes it needs\n3. Use the app's `client_id` in the flows below. Orshot OAuth clients are public clients: PKCE (`S256`) is required and no client secret is used.\n\nDiscovery: `https://api.orshot.com/.well-known/oauth-authorization-server` (dynamic client registration is available at `registration_endpoint`).\n\n### OAuth 2.0 Flows\n\n**Authorization Code + PKCE** (web and desktop apps):\n```\nGET https://orshot.com/oauth/authorize?\n  response_type=code&\n  client_id=YOUR_CLIENT_ID&\n  redirect_uri=https://yourapp.com/callback&\n  code_challenge=BASE64URL_SHA256_OF_VERIFIER&\n  code_challenge_method=S256&\n  scope=workspace:templates:read workspace:templates:write render:generate\n```\n\n**Device Flow** (CLI/headless):\n```\nPOST https://api.orshot.com/v1/oauth/device/code\n  client_id=YOUR_CLIENT_ID&\n  scope=workspace:templates:read render:generate\n```\n\n### Token Exchange\n\n```\nPOST https://api.orshot.com/v1/oauth/token\n  grant_type=authorization_code&\n  code=AUTH_CODE&\n  client_id=YOUR_CLIENT_ID&\n  code_verifier=THE_PKCE_VERIFIER&\n  redirect_uri=https://yourapp.com/callback\n```\n\nRefresh with `grant_type=refresh_token` when the `offline_access` scope was granted. Store tokens server-side or in the user's own environment; never place them in prompts, logs, or shared files.\n\n### Available Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `workspace:read` | List and read workspace details |\n| `workspace:templates:read` | List and read templates |\n| `workspace:templates:write` | Update template modifications via API |\n| `render:generate` | Generate images, PDFs, and videos |\n| `mcp:access` | Access via Model Context Protocol |\n| `offline_access` | Long-lived refresh tokens |\n\nFor endpoint details: fetch `https://orshot.com/docs/developers/oauth-endpoints.md`\n\n## White-Label Embed (Orshot Embed)\n\nEmbed Orshot's template editor into your application as a white-label component. Users can design and customize templates directly in your app.\n\nFor detailed setup: fetch `https://orshot.com/docs/orshot-embed/introduction.md`\n\n### Quick Setup (iframe)\n\n```html\n<iframe\n  src=\"https://orshot.com/embeds/YOUR_EMBED_ID?userId=USER_123\"\n  width=\"100%\"\n  height=\"600\"\n  frameborder=\"0\"\n></iframe>\n```\n\n### React SDK\n\n```bash\nnpm install @orshot/react\n```\n\n```jsx\nimport { OrshotEmbed } from \"@orshot/react\";\n\n<OrshotEmbed\n  embedId=\"YOUR_EMBED_ID\"\n  userId=\"user_123\"\n  token=\"JWT_TOKEN\"\n  onRender={(data) => console.log(\"Rendered:\", data)}\n  onSave={(data) => console.log(\"Saved:\", data)}\n/>\n```\n\n### Vue SDK\n\n```bash\nnpm install @orshot/vue\n```\n\n```vue\n<template>\n  <OrshotEmbed\n    embed-id=\"YOUR_EMBED_ID\"\n    user-id=\"user_123\"\n    @render=\"onRender\"\n    @save=\"onSave\"\n  />\n</template>\n```\n\n### Key Features\n\n- **Per-user templates**: Pass `userId` to give each user their own template copies\n- **JWT authentication**: Secure embed access with domain whitelist validation\n- **Webhooks**: Get notified on render completion, template save, etc.\n- **Custom buttons**: Add your own action buttons to the editor toolbar\n- **PostMessage API**: Control the embed programmatically from your app\n\nFor React SDK details: fetch `https://orshot.com/docs/orshot-embed/react-sdk.md`\nFor Vue SDK details: fetch `https://orshot.com/docs/orshot-embed/vue-sdk.md`\nFor webhook setup: fetch `https://orshot.com/docs/orshot-embed/webhooks.md`\n\n## Migrating from Another Template-Render API\n\nThe API-call swap is the easy part. The real work is recreating the design, because another platform's template JSON cannot be imported:\n\n1. **Recreate the template in Orshot Studio** (or via `POST /v1/studio/templates/create`). Rebuild the layout, then mark every dynamic layer parameterizable with a clear `parameterId`.\n2. **Map old field names → Orshot `parameterId`s.** Other APIs key by layer name or by fixed keys such as `image_url`; Orshot keys by `parameterId`. Keep a lookup from old names to new ones.\n3. **Swap the API call**: `Authorization: Bearer <ORSHOT_API_KEY>` + `POST /v1/studio/render` with `modifications` as a flat object (page-prefixed keys for multi-page templates).\n4. **Handle sync vs async.** Many render APIs are submit-then-poll; Orshot image and PDF renders return synchronously, so delete the polling/webhook code and read `data.content` from the response. Long video renders use async render jobs (see above).\n5. **Style overrides** that other APIs put in per-layer objects become dot-notation keys here (`\"title.fontSize\": \"48px\"`).\n6. **Verify the response shape** — single page is an object, multi-page is an array (Common Gotchas #7) — before cutting traffic over.\n\nRender URLs on Orshot persist; do not add expiry handling unless you delete renders yourself.\n\n### Links\n\n- Documentation: https://orshot.com/docs\n- API Reference: https://orshot.com/docs/api-reference\n- Integrations: https://orshot.com/integrations\n- MCP Server: https://orshot.com/docs/integrations/mcp-server\n- Templates: https://orshot.com/templates\n- Developer Apps: https://orshot.com/docs/developers\n- Social Publishing: https://orshot.com/docs/publish/introduction\n- Orshot Embed: https://orshot.com/docs/orshot-embed/introduction\n"
Full snapshot data
{
  "description": "Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render jobs) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.",
  "included_files": [],
  "name": "orshot",
  "skill_md_contents": "---\nname: orshot\ndescription: Generate images, PDFs, and videos from templates with Orshot — via the REST API, SDKs (Node/Python/PHP/Ruby), the remote MCP server, or no-code tools (Zapier, Make, n8n, Airtable), and wire those renders into a recurring automation. Use whenever the user mentions Orshot, connects Orshot to an AI agent or MCP client, wants a render to run unattended on a schedule or trigger, or needs to programmatically produce marketing visuals, OG images, social carousels, certificates/invoices/tickets as PDF, or video from templates — including when migrating from another template-based image-generation API. Always load this skill for Orshot tasks, even simple ones, because it carries gotchas (modifications key by parameterId, multi-page needs a page1@ prefix, long video renders need async render jobs) that otherwise cause failed renders. Not for generating standalone AI images with no template or Orshot involved.\nmetadata:\n  author: Rishi Mohan\n  version: \"1.3.0\"\n---\n\n# Orshot – Automated Visual Content Generation\n\n[Orshot](https://orshot.com) is an automated image, PDF, and video generation platform. Design templates in Orshot Studio (or import from Canva/Figma), then generate renders via REST API, SDKs, or no-code integrations.\n\n- **Documentation:** https://orshot.com/docs\n- **API Base URL:** https://api.orshot.com/v1\n\n## When to Use This Skill\n\nUse this skill whenever the user mentions **Orshot**, or when the task is:\n\n- Generating images, PDFs, or videos programmatically from templates\n- Building automated marketing visual pipelines (OG images, ad creatives, thumbnails)\n- Creating dynamic social media content (carousels, posts, stories) and publishing it\n- Generating certificates, invoices, tickets, or reports as PDFs\n- Building image/PDF/video generation into a product\n- Automating visuals with Zapier, Make, n8n, Airtable, or the CLI\n- Connecting Orshot to an AI agent or MCP client (Claude, Cursor, Codex, Windsurf, ChatGPT)\n- Embedding a white-label design editor into an app\n- Migrating from another template-based image/PDF/video generation API\n\n**Don't use this skill** for generating standalone AI images with no template or Orshot involved (a one-off \"make me an image\" request) — that isn't what Orshot does.\n\n## Accessing Detailed Documentation\n\nAny page on orshot.com can be fetched as clean markdown by appending `.md` to the URL or sending `Accept: text/markdown` header. Use this to get detailed, up-to-date information on demand without relying solely on this skill file.\n\n**Examples:**\n```\nhttps://orshot.com/docs/api-reference.md\nhttps://orshot.com/docs/sdks/node.md\nhttps://orshot.com/docs/publish/publish-from-api.md\nhttps://orshot.com/docs/developers/oauth-overview.md\nhttps://orshot.com/docs/orshot-embed/introduction.md\n```\n\n**Key documentation pages:**\n| Topic | URL |\n|-------|-----|\n| API Reference | `https://orshot.com/docs/api-reference.md` |\n| Node.js SDK | `https://orshot.com/docs/sdks/node.md` |\n| Python SDK | `https://orshot.com/docs/sdks/python.md` |\n| PHP SDK | `https://orshot.com/docs/sdks/php.md` |\n| Ruby SDK | `https://orshot.com/docs/sdks/ruby.md` |\n| Studio Templates | `https://orshot.com/docs/orshot-studio/introduction.md` |\n| Style Parameters | `https://orshot.com/docs/orshot-studio/style-parameters.md` |\n| Setting Parameters | `https://orshot.com/docs/orshot-studio/setting-parameters.md` |\n| Image Generation | `https://orshot.com/docs/image-generation.md` |\n| Video Generation | `https://orshot.com/docs/video-generation.md` |\n| PDF Generation | `https://orshot.com/docs/pdf-generation.md` |\n| Social Publishing | `https://orshot.com/docs/publish/introduction.md` |\n| OAuth / Developer Apps | `https://orshot.com/docs/developers.md` |\n| White-Label Embed | `https://orshot.com/docs/orshot-embed/introduction.md` |\n| Integrations | `https://orshot.com/docs/integrations.md` |\n| Dynamic URLs | `https://orshot.com/docs/integrations/dynamic-urls.md` |\n| Webhooks | `https://orshot.com/docs/integrations/webhooks.md` |\n| Error Reference | `https://orshot.com/docs/error-reference.md` |\n\nWhen a user asks about a specific topic, fetch the relevant `.md` URL for the latest details.\n\n## Getting Started\n\n### Authentication\n\nAll API requests require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer <ORSHOT_API_KEY>\n```\n\n[Get your API key](https://orshot.com/docs/quick-start/get-api-key) from **Workspace Settings → API Keys** in the Orshot dashboard.\n\n### Remote MCP Server\n\nOrshot runs a hosted MCP server so agents (Claude, Cursor, Codex, Windsurf, ChatGPT, etc.) can use Orshot as tools — no local install.\n\n- **URL:** `https://mcp.orshot.com/mcp` (transport: `streamable-http`)\n- **Auth:** OAuth 2.0 (the client walks you through it), or send an Orshot API key as a Bearer token\n- **Claude Code:** `claude mcp add --transport http orshot https://mcp.orshot.com/mcp`\n- **Cursor / Windsurf / VS Code / ChatGPT / Codex:** add the URL as a remote/HTTP MCP server\n\nIt exposes tools for rendering, studio + library templates, brand assets, workflows, social accounts, and workspace/logs. Full discovery doc: `https://orshot.com/.well-known/mcp.json`. Setup guide: `https://orshot.com/docs/integrations/mcp-server.md`.\n\n### SDKs\n\n#### Node.js\n\n```bash\nnpm install orshot\n```\n\n```js\nimport { Orshot } from \"orshot\";\nconst orshot = new Orshot(\"<ORSHOT_API_KEY>\");\n\n// Render from template\nconst response = await orshot.renderFromTemplate({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello World\" },\n  responseType: \"base64\", // \"base64\" | \"url\" | \"binary\"\n  responseFormat: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\"\n});\n\n// Generate signed URL\nconst signedUrl = await orshot.generateSignedUrl({\n  templateId: \"open-graph-image-1\",\n  modifications: { title: \"Hello\" },\n  expiresAt: 1744276943,\n  renderType: \"images\",\n  responseFormat: \"png\",\n});\n```\n\n#### Python\n\n```bash\npip install orshot\n```\n\n```python\nimport orshot\nos = orshot.Orshot('<ORSHOT_API_KEY>')\n\nresponse = os.render_from_template({\n  'template_id': 'open-graph-image-1',\n  'modifications': {'title': 'Hello World'},\n  'response_type': 'base64',\n  'response_format': 'png'\n})\n```\n\n#### Other SDKs\n\n- **PHP:** `composer require rishimohan/orshot`\n- **Ruby:** `gem install orshot`\n\n## Common Gotchas (read before rendering)\n\nThe mistakes that most often make Orshot calls fail or return the wrong output:\n\n1. **Modifications key by `parameterId`, not layer name.** `modifications: { \"headline\": \"...\" }` targets the element whose `parameterId` is `headline`. If a value is ignored, the key is wrong — fetch the template's modifications (`GET /v1/studio/templates/:id`) to see the real keys instead of guessing.\n2. **Multi-page templates need a `pageN@` prefix.** Use `\"page1@title\"`, `\"page2@title\"`. A bare `\"title\"` only affects page 1.\n3. **Studio `templateId` is an integer; utility `templateId` is a string.** `/v1/studio/render` takes an integer ID; `/v1/generate/:renderType` takes a string slug like `\"website-screenshot\"`.\n4. **Video output does NOT require video elements.** `format: \"mp4\"`/`\"webm\"`/`\"gif\"` renders any template — motion can come from video elements, page animations (enter/exit/ambient transitions), page-to-page transitions, or generated narration; a fully static page renders as a still clip of its `videoDuration`. Long video renders should use async render jobs (`response.mode: \"async\"`) so they don't hit client timeouts.\n5. **Image/video URLs in modifications must be publicly reachable.** The renderer fetches them server-side, so `localhost`, expired signed URLs, or auth-gated URLs won't load.\n6. **Style overrides use dot notation on the parameterId** — `\"title.fontSize\": \"48px\"`, not a nested object.\n7. **Response shape differs by page count:** single page → `data` is an object (read `data.content`); multi-page/carousel → `data` is an array of `{ page, content }`. Handle both.\n8. **`base64` and `binary` response types don't combine** — `binary` returns one raw file stream.\n9. **Credits vs AI Credits are separate** — normal renders spend credits; `.prompt` (AI) modifications additionally spend AI Credits.\n10. **On `429`, back off** using the `Retry-After` response header.\n\nWhen unsure about a template's parameters, always fetch its modifications first.\n\n## Template Architecture\n\nThis section describes the complete structure of an Orshot template for MCP tools and AI agents.\n\n### Template Structure\n\nAn Orshot template consists of **pages**, each containing a **canvas** and **elements**.\n\n```\nTemplate\n├── id: number | string\n├── name: string\n├── description: string\n├── width: number\n├── height: number\n├── pages_data: Array\n    └── Page\n        ├── id: string (UUID)\n        ├── name: string\n        ├── canvas: CanvasConfig\n            ├── width: number\n            ├── height: number\n            ├── backgroundColor: string\n            ├── backgroundImage: string\n        ├── elements: Element[]\n        ├── modifications: Modification[] (API parameters)\n            ├── id: string\n            ├── type: string\n            ├── element: Element\n            ├── description: string\n        └── thumbnail_url: string | null\n```\n\n### Canvas Configuration\n\n| Property          | Type   | Default         | Description                               |\n| ----------------- | ------ | --------------- | ----------------------------------------- |\n| `width`           | number | 800             | Canvas width in pixels (max: 5000)        |\n| `height`          | number | 800             | Canvas height in pixels (max: 5000)       |\n| `backgroundColor` | string | \"#ffffff\"       | Background color (hex, rgba, or gradient) |\n| `backgroundImage` | string | \"\"              | URL to background image                   |\n| `borderWidth`     | number | 0               | Border width in pixels                    |\n| `borderColor`     | string | \"rgba(0,0,0,1)\" | Border color                              |\n| `borderStyle`     | string | \"solid\"         | Border style (solid, dashed, etc)         |\n\n### Canvas Size Presets\n\n| Name                 | Dimensions | Use Case                        |\n| -------------------- | ---------- | ------------------------------- |\n| Square               | 1080×1080  | Instagram posts, general social |\n| Instagram Story      | 1080×1920  | Stories, Reels, TikTok          |\n| Slide/Presentation   | 1920×1080  | Presentations, slides           |\n| YouTube Thumbnail    | 1280×720   | Video thumbnails                |\n| Twitter Post         | 1600×900   | X/Twitter posts                 |\n| Open Graph           | 1200×630   | Link previews, Facebook         |\n| Pinterest Pin        | 1000×1500  | Pinterest                       |\n| A4 Document          | 2480×3508  | Print documents                 |\n| App Store Screenshot | 1290×2796  | iOS app screenshots             |\n\n### Universal Element Properties\n\nAll elements share these base properties:\n\n| Property            | Type    | Description                                  |\n| ------------------- | ------- | -------------------------------------------- |\n| `id`                | string  | Unique identifier (UUID)                     |\n| `name`              | string  | Display name in layer list (was `layerName`) |\n| `type`              | string  | \"text\", \"image\", \"shape\", \"video\"            |\n| `position`          | object  | `{ x: number, y: number }` from top-left     |\n| `dimensions`        | object  | `{ width: number, height: number }`          |\n| `rotation`          | number  | Rotation in degrees (0-360)                  |\n| `zIndex`            | number  | Layer order (higher = on top)                |\n| `aspectRatioLocked` | boolean | Lock aspect ratio during resize              |\n| `isHidden`          | boolean | Hide element from render                     |\n| `skewX`             | number  | Horizontal skew angle                        |\n| `skewY`             | number  | Vertical skew angle                          |\n\n### Text Element\n\n**Content Types:**\n\n- **Plain text**: `\"Hello World\"` - Standard text string\n- **Multi-line**: Use `\\n` for line breaks: `\"Line 1\\nLine 2\"`\n- **Dynamic via API**: Use `.prompt` modifier for AI-generated text\n\n```javascript\n{\n  type: \"text\",\n  content: string,          // Plain text string\n  layerName: string,        // Display name\n  zIndex: number,\n  rotation: number,\n  position: { x: number, y: number },\n  dimensions: { width: number, height: number },\n  style: {\n    // Typography\n    fontFamily: string,     // e.g., \"Inter\", \"Prata\", \"SF Pro\"\n    fontSize: string,       // e.g., \"48px\"\n    fontWeight: string | number, // \"400\", \"700\", 700\n    fontStyle: string,      // \"normal\", \"italic\"\n    lineHeight: number,     // e.g., 1.2\n    letterSpacing: string,  // e.g., \"0px\", \"2px\"\n\n    // Appearance\n    fill: string,           // Color or gradient\n    color: string,          // Hex or rgba\n    opacity: number,        // 0-1\n    stroke: string,         // Stroke color\n    strokeWidth: string,    // e.g., \"0px\"\n\n    // Alignment & Layout\n    textAlign: string,      // \"left\", \"center\", \"right\"\n    verticalAlign: string,  // \"flex-start\", \"center\", \"flex-end\"\n    textTransform: string,  // \"none\", \"uppercase\"\n    textDecoration: string, // \"none\", \"underline\"\n    textMode: string,       // \"overflow\", \"fit\"\n    paddingX: string,\n    paddingY: string,\n\n    // Borders & Backgrounds\n    borderColor: string,\n    borderWidth: string,\n    borderRadius: string,\n    textBackgroundColor: string,\n    textBackgroundRadius: string,\n    textStrokeColor: string,\n    textStrokeWidth: string,\n\n    // Effects\n    minFontSize: string,    // For \"fit\" mode\n    filter: string,         // e.g., \"blur(0px)\"\n    mixBlendMode: string,   // \"normal\", \"multiply\", etc.\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  // Parameterization\n  parameterizable: boolean,\n  parameterId: string,\n  parameterType: \"text\"\n}\n```\n\n**Gradient text:**\n\n```javascript\ncolor: \"linear-gradient(90deg, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Image Element\n\n**Content Types:**\n\n- **URL** (recommended): `\"https://example.com/image.png\"` - Best for dynamic content\n- **Base64**: `\"data:image/png;base64,iVBORw0KGgo...\"` - For embedded images\n- **Binary**: Raw binary data (API upload only)\n\n```javascript\n{\n  type: \"image\",\n  content: string,          // URL (preferred), base64, or binary\n  isSvg: boolean,\n  layerName: string,\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string, // \"center\", \"top left\"\n\n    // Appearance\n    opacity: number,\n    fill: string,           // Background fill\n    stroke: string,         // Border stroke\n\n    // Borders\n    borderRadius: string,   // \"0px\", \"12px\", \"50%\"\n    borderWidth: string,\n    borderColor: string,\n\n    // Effects\n    filter: string,         // \"blur(2px)\", \"grayscale(100%)\"\n    mixBlendMode: string,\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string,\n\n    svgColor: string        // Recolor monochrome SVGs\n  },\n  parameterType: \"imageUrl\"\n}\n```\n\n### Shape Element\n\n```javascript\n{\n  type: \"shape\",\n  shapeType: string,        // \"rectangle\", \"circle\", \"arrow\"\n  layerName: string,\n  style: {\n    // Fill & Stroke\n    fill: string,           // Color or gradient\n    stroke: string,\n    strokeWidth: string,    // e.g. \"0px\"\n\n    // Dimensions\n    borderRadius: string,   // Rectangle only\n    borderWidth: string,\n    borderColor: string,\n    borderStyle: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Shadows\n    boxShadowX: string,\n    boxShadowY: string,\n    boxShadowBlur: string,\n    boxShadowColor: string,\n    dropShadowX: string,\n    dropShadowY: string,\n    dropShadowBlur: string,\n    dropShadowColor: string\n  },\n  parameterType: \"fill\"\n}\n```\n\n**Gradient fills:**\n\n```javascript\nfill: \"linear-gradient(180deg, rgba(0,0,0,0.7) 0%, transparent 100%)\";\nfill: \"radial-gradient(circle at center, #FF6B6B 0%, #4ECDC4 100%)\";\n```\n\n### Video Element\n\n**Content Types:**\n\n- **URL** (required): `\"https://example.com/video.mp4\"` - Must be a publicly accessible URL\n- Supported formats: MP4, WebM, MOV\n- For best results, use MP4 with H.264 codec\n\n```javascript\n{\n  type: \"video\",\n  content: string,          // Video URL (must be publicly accessible)\n  videoOptions: {\n    loop: boolean,\n    muted: boolean,\n    trim_start_time: string,\n    trim_end_time: string,\n    duration: number | null\n  },\n  style: {\n    // Sizing & Positioning\n    objectFit: string,      // \"contain\", \"cover\", \"fill\"\n    objectPosition: string,\n\n    // Appearance\n    opacity: number,\n    filter: string,\n    mixBlendMode: string,\n\n    // Borders & Shadows\n    borderRadius: string,\n    borderWidth: string,\n    borderColor: string,\n    elementBoxShadowX: string,\n    elementBoxShadowY: string,\n    elementBoxShadowBlur: string,\n    elementBoxShadowColor: string\n  },\n  parameterType: \"videoUrl\"\n}\n```\n\n### Parameterization Best Practices\n\nWhen creating or updating templates, **always ensure all text, image, and video elements are parameterizable** with unique IDs. This enables dynamic content replacement via the API.\n\n#### Required Setup\n\nEvery dynamic element MUST have:\n\n```javascript\n{\n  parameterizable: true,\n  parameterId: \"unique_id\",  // Unique across template, lowercase with underscores\n  parameterType: \"text\" | \"imageUrl\" | \"videoUrl\"\n}\n```\n\n#### Naming Conventions\n\n| Element Type | parameterId Examples                        | parameterType |\n| ------------ | ------------------------------------------- | ------------- |\n| Text         | `headline`, `subtitle`, `cta_text`, `price` | `\"text\"`      |\n| Image        | `product_image`, `logo`, `background_image` | `\"imageUrl\"`  |\n| Video        | `hero_video`, `background_video`            | `\"videoUrl\"`  |\n\n#### Best Practices\n\n1. **Use descriptive IDs:** `product_title` not `text1`\n2. **Be consistent:** Use snake_case across all templates\n3. **Unique per template:** No duplicate parameterIds on same page\n4. **Group logically:** Related elements share naming prefix (e.g., `card_title`, `card_image`)\n\n#### Validation Checklist\n\nBefore finalizing any template update:\n- [ ] All text elements have `parameterizable: true` and unique `parameterId`\n- [ ] All image elements have `parameterizable: true` and unique `parameterId`\n- [ ] All video elements have `parameterizable: true` and unique `parameterId`\n- [ ] No duplicate parameterIds exist on the same page\n- [ ] parameterIds are descriptive and follow snake_case convention\n\n### Design Best Practices\n\n#### Typography Guidelines\n- **Font limit:** Use 2-3 fonts maximum per template\n- **Hierarchy:** Headings should be 1.5-2x larger than body text\n- **Minimum size:** 24px for social media readability\n- **Weights:** Headings 600-900 (bold), Body 400-500 (regular)\n\n**Popular font pairings:**\n- `Prata` + `Inter`\n- `Instrument Serif` + `DM Sans`\n- `Playfair Display` + `Lato`\n- `Montserrat` + `Open Sans`\n\n**Platform-specific fonts:**\n- iOS: `SF Pro Display`, `SF Pro Text`\n- Android: `Google Sans`, `Roboto`\n\n#### Color Guidelines\n**Luxury/Gold palette:**\n- Gold: `#D4AF37`\n- Dark gold: `#B8860B`\n- Light gold: `#F5E7A3`\n\n**iOS system colors:**\n- Blue: `#007AFF`\n- Gray: `#8E8E93`\n- Background: `#F5F5F7`\n\n**Professional dark:**\n- Navy: `#0F172A`\n- Slate: `#1E293B`, `#334155`\n- Muted: `#64748B`, `#94A3B8`\n\n**Best practices:**\n- Ensure 4.5:1 minimum contrast for text readability\n- Use gradients sparingly for premium effects\n- Consistent color palette (3-5 colors max)\n\n#### Layout Guidelines\n- **Edge padding:** 40-60px from canvas edges\n- **Element spacing:** 20-40px between elements\n- **Alignment:** Center for formal, left for modern\n- **Visual flow:** Guide eye with size, color, position\n\n**zIndex ordering:**\n- Background images/colors: 1\n- Overlay shapes: 2-3\n- Text elements: 4-6\n- Interactive elements: 7+\n\n#### Common Operations (Design Automation)\n\n**Add text element:**\n```javascript\naddElement(\"text\", {\n  content: \"Hello World\",\n  fontFamily: \"Inter\",\n  fontSize: 48,\n  fontWeight: \"700\",\n  color: \"#FFFFFF\",\n  x: 100,\n  y: 100,\n  width: 400,\n  height: 60,\n});\n```\n\n**Add shape backdrop:**\n```javascript\naddElement(\"rectangle\", {\n  fill: \"rgba(0,0,0,0.5)\",\n  width: 1080,\n  height: 200,\n  x: 0,\n  y: 800,\n  borderRadius: \"0px\",\n});\n```\n\n**Batch update multiple elements:**\n```javascript\nbatchUpdate([\n  {\n    elementId: \"heading\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { fontSize: 72 } },\n  },\n  {\n    elementId: \"subtitle\",\n    type: \"ORSHOT_UPDATE_ELEMENT\",\n    updates: { style: { color: \"#94A3B8\" } },\n  },\n  { type: \"ORSHOT_UPDATE_CANVAS\", updates: { backgroundColor: \"#0F172A\" } },\n]);\n```\n\n## API Reference\n\n### 1. Render from Studio Template\n\nGenerate images/PDFs/videos from templates designed in Orshot Studio.\n\n**POST** `https://api.orshot.com/v1/studio/render`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123, // Integer - your studio template ID\n    modifications: {\n      title: \"Hello World\",\n      imageUrl: \"https://example.com/photo.jpg\",\n      canvasBackgroundColor: \"#eff2fa\",\n    },\n    response: {\n      type: \"base64\", // \"base64\" | \"url\" | \"binary\"\n      format: \"png\", // \"png\" | \"webp\" | \"jpg\" | \"pdf\" | \"mp4\" | \"webm\" | \"gif\"\n      scale: 1, // 1 = original size, 2 = double\n      includePages: [1, 3], // optional – only for multi-page templates\n      fileName: \"my-render\", // optional – custom filename (without extension)\n    },\n    pdfOptions: {\n      // optional – only when format is \"pdf\"\n      margin: \"20px\",\n      rangeFrom: 1,\n      rangeTo: 2,\n      colorMode: \"rgb\", // \"rgb\" or \"cmyk\"\n      dpi: 300,\n    },\n  }),\n});\n```\n\n**Response (single page):**\n```json\n{\n  \"data\": {\n    \"content\": \"data:image/png;base64,iVBORw0.....\",\n    \"format\": \"png\",\n    \"type\": \"base64\",\n    \"responseTime\": 325.22\n  }\n}\n```\n\n**Response (multi-page/carousel):**\n```json\n{\n  \"data\": [\n    { \"page\": 1, \"content\": \"https://storage.orshot.com/.../image1.png\" },\n    { \"page\": 2, \"content\": \"https://storage.orshot.com/.../image2.png\" }\n  ],\n  \"format\": \"png\",\n  \"type\": \"url\",\n  \"responseTime\": 3166.01,\n  \"totalPages\": 2,\n  \"renderedPages\": 2\n}\n```\n\n### 2. Render from Utility Template\n\nGenerate renders from Orshot's pre-built utility templates (e.g., website screenshots, tweet images).\n\n**POST** `https://api.orshot.com/v1/generate/{renderType}`\n\n- `renderType`: `images` or `pdfs`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/generate/images\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\", // String ID for utility templates\n    response: {\n      format: \"png\",\n      type: \"base64\",\n    },\n    modifications: {\n      websiteUrl: \"https://example.com\",\n      fullCapture: false,\n      delay: 500,\n      width: 1200,\n      height: 1000,\n    },\n  }),\n});\n```\n\n### 3. Generate Signed URL\n\nCreate publicly accessible render URLs without exposing your API key.\n\n**POST** `https://api.orshot.com/v1/signed-url/create`\n\n```js\nawait fetch(\"https://api.orshot.com/v1/signed-url/create\", {\n  method: \"POST\",\n  headers: {\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    templateId: \"website-screenshot\",\n    expiresAt: 1744550160505, // UNIX timestamp, or null for no expiry\n    renderType: \"images\",\n    modifications: {\n      websiteUrl: \"https://example.com\",\n    },\n  }),\n});\n```\n\n### 4. List Studio Templates\n\n**GET** `https://api.orshot.com/v1/studio/templates/all?page=1&limit=10`\n\nResponse includes `data` array of templates and `pagination` object with `page`, `limit`, `total`, `totalPages`.\n\n### 5. Get Studio Template\n\n**GET** `https://api.orshot.com/v1/studio/templates/:templateId`\n\nReturns the template metadata including available `modifications`.\n\n### 6. Delete Studio Template\n\n**DELETE** `https://api.orshot.com/v1/studio/templates/:templateId`\n\n### 7. Duplicate Studio Template\n\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/duplicate`\n\n### 8. Get Profile & Workspaces\n\n**GET** `https://api.orshot.com/v1/me`\n\nReturns your profile and the workspaces your API key or OAuth token can access, including plan info and credit usage.\n\n### 9. Brand Assets API\n\nBrand assets are grouped by type — **images**, **colors**, **fonts**, **videos**, **audio** — each with its own routes under `/v1/brand-assets/{type}/…`.\n\n#### Images\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/images/get`\n- **Upload:** `POST https://api.orshot.com/v1/brand-assets/images/add` — `multipart/form-data` with a `file` field\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/images/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/images/delete/:id`\n\n#### Colors\n- **Get:** `GET https://api.orshot.com/v1/brand-assets/colors/get`\n- **Add:** `POST https://api.orshot.com/v1/brand-assets/colors/add` — body `{ type: \"hex\" | \"gradient\", value, tags? }`\n- **Update tags:** `PATCH https://api.orshot.com/v1/brand-assets/colors/update/:id`\n- **Delete:** `DELETE https://api.orshot.com/v1/brand-assets/colors/delete/:id`\n\n#### Fonts / Videos / Audio\nSame shape — swap the type segment (`fonts`, `videos`, `audio`):\n- **Get:** `GET /v1/brand-assets/{type}/get`\n- **Upload:** `POST /v1/brand-assets/{type}/add` (`multipart/form-data`, `file` field)\n- **Update tags:** `PATCH /v1/brand-assets/{type}/update/:id`\n- **Delete:** `DELETE /v1/brand-assets/{type}/delete/:id`\n\n#### Search\n- **Search assets:** `GET https://api.orshot.com/v1/brand-assets/search`\n\nUse an uploaded asset in a render by passing its hosted URL as an image/video modification value.\n\n### 10. Enterprise API Endpoints\n\nThese endpoints require an Enterprise plan.\n\n#### Create Studio Template\n**POST** `https://api.orshot.com/v1/studio/templates/create`\n\n```js\n{\n  name: \"Product Banner\",        // required, max 255 chars\n  description: \"Banner template\", // optional\n  canvas_width: 1200,             // required, 1-5000\n  canvas_height: 628,             // required, 1-5000\n  pages_data: [...]               // optional - array of page objects with elements\n}\n```\n\n#### Bulk Create Studio Templates\n**POST** `https://api.orshot.com/v1/studio/templates/bulk-create`\nCreate multiple templates at once via CSV or JSON.\n\n#### Update Template\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId`\nUpdate template name and description.\n\n#### Update Template Modifications\n**PATCH** `https://api.orshot.com/v1/studio/templates/:templateId/update-modifications`\nUpdate text and image content in template layers.\n\n#### Generate Template Variants\n**POST** `https://api.orshot.com/v1/studio/templates/:templateId/generate-variants`\nGenerate multiple size variants of a template using AI.\n\n### 11. Dynamic URLs\n\nGenerate images directly from URL parameters:\n\n```\nhttps://api.orshot.com/v1/studio/dynamic-url/my-image?title=Hello%20World&title.fontSize=48px&title.color=%23ff0000\n```\nURL-encode special characters (e.g., `#` → `%23`).\n\n### 12. Template Folders & Sharing\n\nOrganize studio templates into folders and share them:\n\n- **List folders:** `GET https://api.orshot.com/v1/studio/folders`\n- **Create folder:** `POST https://api.orshot.com/v1/studio/folders`\n- **Update folder:** `PATCH https://api.orshot.com/v1/studio/folders/:folderId`\n- **Delete folder:** `DELETE https://api.orshot.com/v1/studio/folders/:folderId`\n- **Move template into folder:** `PATCH https://api.orshot.com/v1/studio/templates/:templateId/folder`\n- **Get template sharing:** `GET https://api.orshot.com/v1/studio/templates/:templateId/share`\n- **Update template sharing:** `POST https://api.orshot.com/v1/studio/templates/:templateId/share`\n\n### 13. Workflows API\n\nWorkflows automate multi-step pipelines (trigger → fetch data → render → publish/deliver). Available to first-party clients (like the Orshot MCP server) and Enterprise workspaces.\n\n- **List workflows:** `GET https://api.orshot.com/v1/workflows`\n- **Create workflow:** `POST https://api.orshot.com/v1/workflows`\n- **Get workflow:** `GET https://api.orshot.com/v1/workflows/:id`\n- **Update workflow:** `PATCH https://api.orshot.com/v1/workflows/:id`\n- **Delete workflow:** `DELETE https://api.orshot.com/v1/workflows/:id`\n- **Run workflow:** `POST https://api.orshot.com/v1/workflows/:id/run`\n- **List runs:** `GET https://api.orshot.com/v1/workflows/:id/runs`\n- **Get a run:** `GET https://api.orshot.com/v1/workflows/:id/runs/:runId`\n- **Validate a workflow:** `POST https://api.orshot.com/v1/workflows/validate`\n- **List available nodes:** `GET https://api.orshot.com/v1/workflows/nodes`\n- **Get/update sharing:** `GET` / `POST https://api.orshot.com/v1/workflows/:id/share`\n\n## Render Configuration\n\n### Dynamic Parameters\n\nOverride template styles, content, and behavior at render time using dot notation.\n\n#### Style Parameters\n\nFormat: `parameterId.property`\n\n```json\n{\n  \"modifications\": {\n    \"title\": \"Hello World\",\n    \"title.fontSize\": \"48px\",\n    \"title.color\": \"#ff0000\",\n    \"title.fontFamily\": \"Roboto\",\n    \"title.textAlign\": \"center\",\n    \"logo.borderRadius\": \"50%\",\n    \"logo.objectFit\": \"cover\"\n  }\n}\n```\n\n**Text properties:** `fontSize`, `fontWeight`, `fontStyle`, `fontFamily`, `lineHeight`, `letterSpacing`, `textAlign`, `verticalAlign`, `textDecoration`, `textTransform`, `color`, `backgroundColor`, `backgroundRadius`, `textStrokeWidth`, `textStrokeColor`, `opacity`, `filter`, `dropShadowX/Y/Blur/Color`\n\n**Image properties:** `objectFit`, `objectPosition`, `borderRadius`, `borderWidth`, `borderColor`, `boxShadowX/Y/Blur/Color`, `opacity`, `filter`\n\n**Shape properties:** `fill`, `stroke`, `strokeWidth`, `borderRadius`, `opacity`\n\n**Position/Size (all elements):** `x`, `y`, `width`, `height`\n\nProperty names are **case-insensitive**.\n\n#### Multi-Page Templates\n\nPrefix modifications with page number:\n```json\n{\n  \"modifications\": {\n    \"page1@title\": \"Page 1 Title\",\n    \"page2@title\": \"Page 2 Title\",\n    \"page1@title.fontSize\": \"48px\"\n  }\n}\n```\n\n#### AI Content Generation (.prompt)\n\nGenerate text or images using AI:\n```json\n{\n  \"modifications\": {\n    \"headline.prompt\": \"Write a catchy headline about coffee\",\n    \"background.prompt\": \"A serene mountain landscape at sunset\"\n  }\n}\n```\n- `.prompt` on a text element generates copy; on an image element it generates imagery. The underlying AI models are managed by Orshot and may change over time — don't hardcode a specific model. AI modifications consume AI Credits.\n\n#### Interactive Links (.href)\n\nAdd clickable links in PDF outputs:\n```json\n{\n  \"modifications\": {\n    \"cta_button.href\": \"https://example.com/signup\",\n    \"logo.href\": \"https://company.com\"\n  },\n  \"response\": { \"format\": \"pdf\" }\n}\n```\n\n#### Video Parameters\n\nControl video elements dynamically:\n```json\n{\n  \"modifications\": {\n    \"bgVideo\": \"https://example.com/video.mp4\",\n    \"bgVideo.trimStart\": 5,\n    \"bgVideo.trimEnd\": 15,\n    \"bgVideo.muted\": false,\n    \"bgVideo.loop\": true\n  },\n  \"response\": { \"format\": \"mp4\" }\n}\n```\n\n### Video Render Example\n\nRender templates with video elements as MP4, WebM, or GIF:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: {\n      videoElement: \"https://example.com/custom-video.mp4\",\n      \"videoElement.trimStart\": 0,\n      \"videoElement.trimEnd\": 10,\n      \"videoElement.muted\": false,\n      \"videoElement.loop\": true,\n    },\n    videoOptions: {\n      trimStart: 0,\n      trimEnd: 20,\n      muted: true,\n      loop: true,\n    },\n    response: {\n      type: \"url\",\n      format: \"mp4\",\n    },\n  }),\n});\n```\n\n### Render-level `videoOptions` (top level, sibling of `modifications`)\n\nControls the whole video render, not one element:\n\n```json\n{\n  \"templateId\": 123,\n  \"modifications\": { \"page1@headline\": \"Launch day\" },\n  \"videoOptions\": {\n    \"duration\": 12,\n    \"fps\": 30,\n    \"muted\": false,\n    \"combinePages\": true,\n    \"pageTransition\": \"fade\",\n    \"pageTransitionDuration\": 0.5,\n    \"audioSource\": \"https://example.com/track.mp3\",\n    \"subtitleSource\": \"https://example.com/voiceover.mp3\",\n    \"captionPreset\": \"karaoke\"\n  },\n  \"response\": { \"type\": \"url\", \"format\": \"mp4\" }\n}\n```\n\n- `duration`, `fps` (≤30), `quality`, `muted`, `trimStart`+`trimEnd` (must be paired).\n- `combinePages: true` merges a multi-page template into ONE video, with `pageTransition` (42 types — fades, slides, wipes, zooms, blurs and more) and `pageTransitionDuration` (0–2s, default 0.5) between scenes. A string `audioSource` then lays one continuous soundtrack across the combined video — the recipe for multi-scene videos.\n- `audioSource` swaps an existing URL audio track at render time while keeping its saved trim/fades/volume/offset/loop. Generated narration changes through `tts.parameterId` instead (see Audio below).\n- Captions: `subtitleSource` (a media URL to auto-transcribe, or a `.srt`/`.vtt` used as-is), `captionPreset` (`pill` | `minimal` | `karaoke` | `bold` | `outline` | `neon`), plus fine-grained `subtitleMode` (`phrase`|`word-by-word`), `subtitleWordReveal` (`appear`|`highlight`|`align`), `subtitleSplit` (`word`|`letter`), `subtitleMaxWordsPerLine` or `subtitleMaxCharsPerLine`, and `subtitle*` styling keys. Prefer authored `page.subtitle` with source `\"auto-audio\"` so captions word-sync automatically.\n\n### Async render jobs (long videos — use this from automations)\n\nA synchronous render holds the HTTP connection for the full render time; video routinely outlives client timeouts (Zapier ~30s, many MCP clients 120s). Add `response.mode: \"async\"`:\n\n```\nPOST /v1/studio/render  { ..., \"response\": { \"mode\": \"async\", \"format\": \"mp4\" } }\n  → 202 with a job object in ~100ms\nGET  /v1/studio/render-jobs/:id          → poll the job (queued → processing → succeeded/failed/canceled)\nGET  /v1/studio/render-jobs?status=&limit=  → newest-first list\nPOST /v1/studio/render-jobs/:id/cancel   → cancel while not terminal\n```\n\nThe job's `result` is byte-identical to the sync response. Pass a `webhook_url` to get the settled job POSTed to you instead of polling. Jobs time out at 15 minutes.\n\n### Page animations & ambient motion (video templates)\n\nElements animate via a page-level `transitions` object on the element — no video file needed:\n\n```json\n{\n  \"transitions\": {\n    \"enter\": { \"type\": \"slideInUp\", \"duration\": 0.5, \"easing\": \"ease-out\" },\n    \"showAt\": 0.4,\n    \"ambient\": { \"type\": \"kenBurns\", \"customValues\": { \"zoom\": 1.12, \"panX\": 6 } },\n    \"exit\": { \"type\": \"fadeOut\", \"duration\": 0.3 },\n    \"hideAt\": 3.6\n  }\n}\n```\n\n- Three phases: `enter` → `ambient` (plays the element's whole visible life) → `exit`. `showAt`/`hideAt` are siblings of `enter`/`exit`, never nested inside them (the #1 authoring bug).\n- Ambient types: `kenBurns` (runs once, stretched across visibility — give every full-bleed photo one), plus looping `pulse`, `float`, `drift`, `wiggle`, `shake`, `slowRotate`, `breathe`. `shake`/`slowRotate` need `easing: \"linear\"` or the loop seam shows.\n- All timing is tunable per render via dot-notation modifications: `\"photo.showAt\": 2.1`, `\"photo.enterType\": \"scaleIn\"`, `\"photo.ambientType\": \"kenBurns\"`, etc.\n\n### Audio: soundtracks & AI voiceover (video templates)\n\nEach page carries `audioTracks` (max 10, page-level, mixed into every mp4/webm render):\n\n- **Existing audio**: `{ \"url\": \"https://...\", \"trimStart\": 33.2, \"trimEnd\": 44.1, \"fadeOut\": 1, \"volume\": 1, \"loop\": false, \"offset\": 0 }` — trim to the song's best part; `videoOptions.audioSource` swaps the file at render keeping these settings.\n- **AI voiceover**: `{ \"tts\": { \"text\": \"words to speak\", \"voiceId\": \"...\", \"modelId\": \"flash\", \"parameterId\": \"voiceover\" } }` instead of `url`. Synthesized when the template is saved (metered voice minutes; identical scripts are cached and free on re-render). Narration length drives the page length, and `page.subtitle` with source `\"auto-audio\"` gives word-synced captions with no transcription step.\n- **Generated ambient/SFX**: `{ \"sfx\": { \"prompt\": \"warm modern pulse, instrumental, seamless loop\", \"seconds\": 12 }, \"volume\": 0.12, \"loop\": true }`.\n- `tts.parameterId` makes the SPOKEN TEXT a render parameter: `modifications: { \"voiceover\": \"new words\" }` (multi-page: `\"page1@voiceover\"`) re-voices the video per render; `\"voiceover.voiceId\"` / `\"voiceover.modelId\"` tune the voice.\n- Budget narration at ~2 words/second plus 1 second of headroom for the page duration.\n- Via MCP, call `orshot_get_audio_authoring_options` first — it returns valid voice ids, models and the workspace's voice allowance in one read-only call.\n\n### Smart Resize (one design → any size)\n\nRender a template at a **different canvas size without redesigning it** — the layout is deterministically re-solved to fit (elements re-anchor, backgrounds stretch, groups move together). Set these on the `response` object:\n\n- **`size`** — *replace* the render size. A preset slug (`\"instagram-story\"`, `\"og-image\"`, `\"youtube-thumbnail\"`, …), a `\"WIDTHxHEIGHT\"` string (`\"1080x1920\"`), or use `width` + `height` (10–5000px each).\n- **`extraSizes`** — *add* the same design at extra sizes in one call. An array (`[\"1080x1920\", \"1080x1080\"]`) or a named object (`{ story: \"1080x1920\", square: \"1080x1080\" }`). Each output gains a nested `extraSizes` array of `{ size, width, height, content }`.\n\n```json\n{\n  \"templateId\": 123,\n  \"modifications\": { \"title\": \"Launch Day\" },\n  \"response\": {\n    \"type\": \"url\",\n    \"format\": \"png\",\n    \"size\": \"1200x630\",\n    \"extraSizes\": [\"1080x1920\", \"1080x1080\"]\n  }\n}\n```\n\nImage formats only (`png`/`jpg`/`webp`/`avif`), up to 50 extra outputs per call. Each extra output is billed like a page (1 credit). Saved sizes from the Studio Smart Resize panel reproduce their approved preview exactly.\n\n### Response Types\n\n| Type     | Description                                     |\n| -------- | ----------------------------------------------- |\n| `url`    | Returns a hosted URL to the rendered file       |\n| `base64` | Returns base64-encoded content as a string      |\n| `binary` | Returns binary file content for custom handling |\n\n### Response Formats\n\n| Format | Type  | Notes                                      |\n| ------ | ----- | ------------------------------------------ |\n| `png`  | Image | Best quality, larger size                  |\n| `webp` | Image | Smaller size, good quality                 |\n| `jpg`  | Image | Compressed, no transparency                |\n| `avif` | Image | Smallest size, modern browsers             |\n| `pdf`  | Doc   | Supports multi-page, clickable links, CMYK |\n| `mp4`  | Video | H.264, requires video elements in template |\n| `webm` | Video | VP9, web-optimized                         |\n| `mov`  | Video | QuickTime container                        |\n| `mkv`  | Video | Matroska container                         |\n| `gif`  | Video | Animated, no audio support                 |\n\n### Render Usage (credits)\n\nUsage is measured in **credits**. 1 credit = 1 image, 1 PDF page, or 1 second of video.\n\n| Output               | Cost                     |\n| -------------------- | ------------------------ |\n| Image (PNG/JPG/WebP/AVIF) | 1 credit per image  |\n| PDF                  | 1 credit per page        |\n| Video (MP4/WebM/MOV/MKV/GIF) | 1 credit per second |\n\nMulti-page templates and Smart Resize extra sizes: each output page/size counts as its own credit. AI modifications (`.prompt`) additionally consume AI Credits.\n\n## Common Recipes\n\nEnd-to-end patterns for the jobs people most often automate with Orshot.\n\n### OG image on every deploy\nRender a studio template to a stable hosted URL and drop it into your `<meta>` tags.\n```js\nconst { data } = await fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: post.title, author: post.author },\n    response: { type: \"url\", format: \"png\", size: \"og-image\" },\n  }),\n}).then((r) => r.json());\n// <meta property=\"og:image\" content={data.content} />\n```\n\n### Bulk-generate from a CSV (certificates, badges, invoices)\nLoop rows and render one PDF each. Describe image layers with `.alt` for accessible PDFs.\n```js\nfor (const row of rows) {\n  await fetch(\"https://api.orshot.com/v1/studio/render\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\", Authorization: \"Bearer <KEY>\" },\n    body: JSON.stringify({\n      templateId: 456,\n      modifications: { name: row.name, course: row.course, date: row.date },\n      response: { type: \"url\", format: \"pdf\" },\n      pdfOptions: { title: `${row.name} — Certificate` },\n    }),\n  });\n}\n```\n\n### Render and auto-post to social in one call\nAdd a `publish` object (see Social Publishing) — no second request.\n```js\nbody: JSON.stringify({\n  templateId: 123,\n  modifications: { title: \"Launch day!\" },\n  response: { type: \"url\", format: \"png\" },\n  publish: { accounts: [1, 2], content: \"We just shipped 🚀\" },\n});\n```\n\n### One design, every social size (Smart Resize)\n```js\nresponse: { type: \"url\", format: \"png\", extraSizes: [\"1080x1920\", \"1080x1080\", \"1200x630\"] }\n// each output gains a nested extraSizes[] of { size, width, height, content }\n```\n\n### Automate a recurring pipeline (Workflows)\nCreate a workflow (trigger → fetch data → render → publish/deliver) with `POST /v1/workflows`, then trigger it with `POST /v1/workflows/:id/run`. Inspect results via `GET /v1/workflows/:id/runs`. Best driven through the MCP server or the Workflows API on Enterprise/first-party clients.\n\nSee **Setting Up Recurring Automation** below for the full playbook, including how\nto wire it into an n8n/Make/Zapier setup the user already runs.\n\n## Setting Up Recurring Automation\n\nA one-off render is a demo. The value shows up when it runs unattended. Pick the\nroad that matches where the user ALREADY works, not the one easiest to describe.\n\n### Choose the road first\n\nCheck what they already run before recommending anything. `GET /v1/workspace/logs?limit=100`\nreturns a `source` on every row, which answers it factually:\n\n| `source` in their logs | They already run | Lead with |\n| --- | --- | --- |\n| `n8n-integration` | n8n | Add a node to the n8n workflow they have |\n| `orshot-make` | Make | Add a module to their scenario |\n| `zapier-integration` | Zapier | Add an action to their Zap |\n| `orshot-pipedream` | Pipedream | Add a step |\n| `orshot-*-sdk`, `api`, `cli` | Their own code | Write the call into their repo |\n| only `playground` / `orshot-mcp-server` | Nothing yet | Offer Orshot Workflows |\n\nNever recommend a second orchestrator to someone who already has one. A user with a\nlive n8n setup wants this template added to it, not a new tool to learn.\n\nIf you have browser control or repo access, offer to DO the setup rather than\ndescribe it. Whichever road you take, **run it once and show the resulting image\nURL** before calling it done.\n\n### Road A — Orshot Workflows (no orchestrator yet)\n\nOrshot runs the whole loop: trigger, render, deliver. Over MCP:\n\n1. `orshot_suggest_workflows` with the `templateId` — returns automations that\n   fit this template, with draft-ready steps\n2. `orshot_list_workflow_nodes` — use the EXACT node keys it returns. Invented\n   keys (e.g. `render_studio_template`, `slack_send`) fail as `unknown node`;\n   the real keys are `render`, `slack`, and so on\n3. `orshot_list_connected_integrations` — see what is already connected\n4. `orshot_get_workflow_connection_data` — resolve \"my content sheet\" into a\n   concrete id, and confirm the name back to the user\n5. `orshot_validate_workflow` — read `errors`, `warnings` AND `gated`. A `gated`\n   entry means the step is not included in the workspace's current plan, not a\n   config problem: the draft still saves, only activation is blocked, so tell\n   the user that instead of editing steps\n6. `orshot_create_workflow` with `status: \"draft\"`, then share the edit link from\n   the result — it opens pre-configured in their dashboard\n7. `orshot_run_workflow` — prove it works, then they activate\n\n**Scaffolding shapes.** If they have no data source ready, use\n`webhook` → `webhook_body` → `render` → `orshot_url`. It is the only\ntrigger→source pair needing no OAuth AND it validates clean with empty configs,\nso it is runnable immediately and they can POST to it from cron, their app, n8n,\nor Make. A bare `schedule` → `render` is NOT runnable — it fails with \"no data\nsource\"; a schedule needs a source step after it.\n\n### Road B — their existing automation platform\n\nEvery platform needs the same three things: endpoint, auth header, modification\nkeys. Get the exact keys first — never guess them:\n\n```\norshot_get_studio_template_modifications   (or GET /v1/studio/templates/:id/modifications)\n```\n\nThen the call to drop in:\n\n```http\nPOST https://api.orshot.com/v1/studio/render\nAuthorization: Bearer <ORSHOT_API_KEY>\nContent-Type: application/json\n\n{\n  \"templateId\": 1234,\n  \"modifications\": { \"headline\": \"{{ row.title }}\", \"hero_image\": \"{{ row.image_url }}\" },\n  \"response\": { \"type\": \"url\", \"format\": \"png\" }\n}\n```\n\nThe response contains the rendered URL — map it to whatever the next step needs.\n\n- **n8n** — HTTP Request node, POST, header auth, body as above. A dedicated\n  Orshot node also exists in the n8n library.\n- **Make** — HTTP \"Make a request\" module, or the Orshot app modules.\n- **Zapier** — Webhooks by Zapier → POST, or the Orshot Zapier app.\n- **Pipedream** — HTTP step; Orshot components are published.\n- **Own code / cron** — any HTTP client. Use `response.type: \"url\"` in production\n  so you are not moving base64 around, and keep the API key in an environment\n  variable, never inline.\n\nRecurring triggers that work well: a new spreadsheet/Airtable/Notion row, a\nschedule, an inbound webhook, or a CMS publish event.\n\n### Verify before you call it done\n\n1. Trigger one real run\n2. `orshot_list_workspace_logs` (or `GET /v1/workspace/logs`) — the new render\n   appears with the `source` of whatever fired it, proving which system made it\n3. Show the user the image URL from that run\n\nIf the render 403s, read the body: an expired key, an inactive subscription, and\na plan limit all return 403 with different messages. A plan block is not an auth\nproblem — do not retry it as one.\n\n## Integrations & Helps\n\n### Integrations Service Support\nOrshot connects with:\n- **No-code:** Zapier, Make (Integromat), n8n, Pipedream, Airtable\n- **Storage:** Amazon S3, Cloudflare R2, Google Drive, Dropbox\n- **Notifications:** Slack, Webhooks\n- **Design:** Figma Plugin, Canva Import, Polotno Import\n- **CLI:** `npx orshot-cli` for terminal-based generation\n- **MCP Server:** Use with Claude, Cursor, Windsurf via MCP protocol\n- **Embed:** White-label design editor for your app (React SDK, Vue SDK, iframe)\n\n### Common Error Codes\n\n| Code | Error                          | Fix                                          |\n| ---- | ------------------------------ | -------------------------------------------- |\n| 400  | `templateId missing`           | Add `templateId` to request body             |\n| 400  | `Invalid API Key`              | Generate new key from dashboard              |\n| 403  | `Authorization header missing` | Add `Authorization: Bearer <KEY>` header     |\n| 403  | `Subscription inactive`        | The workspace subscription is not active; check it in the dashboard |\n| 403  | `Template not found`           | Verify template ID belongs to your workspace |\n| 403  | `ai-video-not-on-plan`         | AI video is not included in the workspace's current plan; use stock footage or a Brand Library video instead |\n\n### General Best Practices\n\n1. **Use `url` response type** for production – avoids large base64 payloads\n2. **Use `webp` format** for smaller file sizes with good quality\n3. **Test in Playground** before coding – each template has an interactive playground\n4. **Use style parameters** instead of creating multiple template variants\n5. **Use consistent units** – stick to `px` for sizes\n6. **Multi-page prefix** – always use `page1@paramId` format for carousel templates\n7. **Cache renders** – use `?cache=false` query param to bypass cache when needed\n8. **Handle errors** – check for 403/400 status codes and parse error messages\n\n## Social Publishing\n\nPublish rendered images/videos directly to 13+ social platforms via API. Supports Twitter/X, Instagram, LinkedIn, Pinterest, Facebook, TikTok, YouTube, Threads, Bluesky, Reddit, Telegram, Snapchat, and Google Business.\n\nFor detailed setup: fetch `https://orshot.com/docs/publish/introduction.md`\n\n### Connect Accounts\n\nConnect social accounts in **Workspace Settings → Social Accounts** in the Orshot dashboard. Each connected account gets a numeric ID used in API calls.\n\n### Publish from API\n\nAdd the `publish` object to any render request:\n\n```js\nawait fetch(\"https://api.orshot.com/v1/studio/render\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer <ORSHOT_API_KEY>\",\n  },\n  body: JSON.stringify({\n    templateId: 123,\n    modifications: { title: \"New Post\" },\n    response: { type: \"url\", format: \"png\" },\n    publish: {\n      accounts: [1, 3],          // Social account IDs\n      content: \"Check this out!\", // Caption text (max 5000 chars)\n      isDraft: false,             // true = save as draft\n      schedule: {                 // Optional: schedule for later\n        scheduledFor: \"2026-04-25T14:00:00Z\"\n      },\n      timezone: \"America/New_York\",\n      platformOptions: {          // Per-account overrides (keyed by account ID)\n        \"1\": { firstComment: \"https://example.com\" },     // LinkedIn\n        \"3\": { title: \"Pin Title\", link: \"https://...\" }   // Pinterest\n      }\n    }\n  }),\n});\n```\n\n**Response includes publish status:**\n```json\n{\n  \"data\": { \"content\": \"https://storage.orshot.com/...\" },\n  \"publish\": [\n    { \"platform\": \"twitter\", \"username\": \"acme\", \"status\": \"published\", \"url\": \"https://x.com/...\" },\n    { \"platform\": \"linkedin\", \"username\": \"acme\", \"status\": \"scheduled\" }\n  ]\n}\n```\n\n**Format compatibility:**\n- PDF cannot be published to any platform\n- Video (mp4/webm/gif) cannot be published to Google Business\n- Image (png/jpg/webp) cannot be published to TikTok or YouTube (video-only)\n\n## Developer Apps & OAuth\n\nBuild third-party integrations with Orshot using OAuth 2.0. Register an app, authenticate users, and access their workspaces on their behalf.\n\nFor detailed setup: fetch `https://orshot.com/docs/developers/oauth-overview.md`\n\n### Register an App\n\n1. Go to **Workspace Settings → Developer Apps** in the Orshot dashboard\n2. Create a new app with a name, redirect URI, and the scopes it needs\n3. Use the app's `client_id` in the flows below. Orshot OAuth clients are public clients: PKCE (`S256`) is required and no client secret is used.\n\nDiscovery: `https://api.orshot.com/.well-known/oauth-authorization-server` (dynamic client registration is available at `registration_endpoint`).\n\n### OAuth 2.0 Flows\n\n**Authorization Code + PKCE** (web and desktop apps):\n```\nGET https://orshot.com/oauth/authorize?\n  response_type=code&\n  client_id=YOUR_CLIENT_ID&\n  redirect_uri=https://yourapp.com/callback&\n  code_challenge=BASE64URL_SHA256_OF_VERIFIER&\n  code_challenge_method=S256&\n  scope=workspace:templates:read workspace:templates:write render:generate\n```\n\n**Device Flow** (CLI/headless):\n```\nPOST https://api.orshot.com/v1/oauth/device/code\n  client_id=YOUR_CLIENT_ID&\n  scope=workspace:templates:read render:generate\n```\n\n### Token Exchange\n\n```\nPOST https://api.orshot.com/v1/oauth/token\n  grant_type=authorization_code&\n  code=AUTH_CODE&\n  client_id=YOUR_CLIENT_ID&\n  code_verifier=THE_PKCE_VERIFIER&\n  redirect_uri=https://yourapp.com/callback\n```\n\nRefresh with `grant_type=refresh_token` when the `offline_access` scope was granted. Store tokens server-side or in the user's own environment; never place them in prompts, logs, or shared files.\n\n### Available Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `workspace:read` | List and read workspace details |\n| `workspace:templates:read` | List and read templates |\n| `workspace:templates:write` | Update template modifications via API |\n| `render:generate` | Generate images, PDFs, and videos |\n| `mcp:access` | Access via Model Context Protocol |\n| `offline_access` | Long-lived refresh tokens |\n\nFor endpoint details: fetch `https://orshot.com/docs/developers/oauth-endpoints.md`\n\n## White-Label Embed (Orshot Embed)\n\nEmbed Orshot's template editor into your application as a white-label component. Users can design and customize templates directly in your app.\n\nFor detailed setup: fetch `https://orshot.com/docs/orshot-embed/introduction.md`\n\n### Quick Setup (iframe)\n\n```html\n<iframe\n  src=\"https://orshot.com/embeds/YOUR_EMBED_ID?userId=USER_123\"\n  width=\"100%\"\n  height=\"600\"\n  frameborder=\"0\"\n></iframe>\n```\n\n### React SDK\n\n```bash\nnpm install @orshot/react\n```\n\n```jsx\nimport { OrshotEmbed } from \"@orshot/react\";\n\n<OrshotEmbed\n  embedId=\"YOUR_EMBED_ID\"\n  userId=\"user_123\"\n  token=\"JWT_TOKEN\"\n  onRender={(data) => console.log(\"Rendered:\", data)}\n  onSave={(data) => console.log(\"Saved:\", data)}\n/>\n```\n\n### Vue SDK\n\n```bash\nnpm install @orshot/vue\n```\n\n```vue\n<template>\n  <OrshotEmbed\n    embed-id=\"YOUR_EMBED_ID\"\n    user-id=\"user_123\"\n    @render=\"onRender\"\n    @save=\"onSave\"\n  />\n</template>\n```\n\n### Key Features\n\n- **Per-user templates**: Pass `userId` to give each user their own template copies\n- **JWT authentication**: Secure embed access with domain whitelist validation\n- **Webhooks**: Get notified on render completion, template save, etc.\n- **Custom buttons**: Add your own action buttons to the editor toolbar\n- **PostMessage API**: Control the embed programmatically from your app\n\nFor React SDK details: fetch `https://orshot.com/docs/orshot-embed/react-sdk.md`\nFor Vue SDK details: fetch `https://orshot.com/docs/orshot-embed/vue-sdk.md`\nFor webhook setup: fetch `https://orshot.com/docs/orshot-embed/webhooks.md`\n\n## Migrating from Another Template-Render API\n\nThe API-call swap is the easy part. The real work is recreating the design, because another platform's template JSON cannot be imported:\n\n1. **Recreate the template in Orshot Studio** (or via `POST /v1/studio/templates/create`). Rebuild the layout, then mark every dynamic layer parameterizable with a clear `parameterId`.\n2. **Map old field names → Orshot `parameterId`s.** Other APIs key by layer name or by fixed keys such as `image_url`; Orshot keys by `parameterId`. Keep a lookup from old names to new ones.\n3. **Swap the API call**: `Authorization: Bearer <ORSHOT_API_KEY>` + `POST /v1/studio/render` with `modifications` as a flat object (page-prefixed keys for multi-page templates).\n4. **Handle sync vs async.** Many render APIs are submit-then-poll; Orshot image and PDF renders return synchronously, so delete the polling/webhook code and read `data.content` from the response. Long video renders use async render jobs (see above).\n5. **Style overrides** that other APIs put in per-layer objects become dot-notation keys here (`\"title.fontSize\": \"48px\"`).\n6. **Verify the response shape** — single page is an object, multi-page is an array (Common Gotchas #7) — before cutting traffic over.\n\nRender URLs on Orshot persist; do not add expiry handling unless you delete renders yourself.\n\n### Links\n\n- Documentation: https://orshot.com/docs\n- API Reference: https://orshot.com/docs/api-reference\n- Integrations: https://orshot.com/integrations\n- MCP Server: https://orshot.com/docs/integrations/mcp-server\n- Templates: https://orshot.com/templates\n- Developer Apps: https://orshot.com/docs/developers\n- Social Publishing: https://orshot.com/docs/publish/introduction\n- Orshot Embed: https://orshot.com/docs/orshot-embed/introduction\n"
}

SHA-256 of public snapshot: c3a7e072725a11a0ed765146ada439217ddbf459ddf5e10d44676849fc932ce1