Agents & API

A programmatic render API built for agents: deterministic, computable pricing, and scoped keys with bounded spend — so an agent can estimate cost and render without a human in the loop.

Rendering is live for MP4, WebM, GIF, SVG, PNG, PDF and embeddable widgets, billed per the credit formula; estimate and validate are live and free. Current health is on status. We don't claim an uptime SLA yet.

Organizations & service accounts

Get a key

Create a service account from your dashboard: Account → Service Accounts. Pick a name, scopes, and an optional spend cap. The key (pp_sa_…) is shown once — copy it then; only a hash is stored.

Authenticate

Send the key as a bearer token:

Authorization: Bearer pp_sa_xxxxxxxxxxxxxxxx

Scopes

ScopeGrants
render:createSubmit renders and spend credits on them
render:readRead render jobs and fetch artifacts
scene:validateValidate a scene without rendering it (free)
inference:invokeGenerate designs through /v1/inference
mcp:invokeDrive the canvas over MCP (/mcp, /rpc) and join collab rooms
graph:proposePropose design-graph candidates for admin review — renders each one and spends credits
scene:editRead and edit cloud-editor scenes (/v1/scenes) — $0.001 per call, charged only when it succeeds
plan:createDraft and estimate generation plans
plan:approveApprove generation plans and commit budgets
credits:purchaseStart a credit-pack checkout for you to pay — credits land on your account

A request without one of the required scopes is rejected with insufficient_scope. A spend cap is enforced before a render is held — once a key has charged up to its cap, further renders return 402.

Pricing is computable

Cost is deterministic, so an agent can bound it before calling:

credits = ceil(duration_s × fps_factor × resolution_factor × format_factor)
1 credit = 1s of 1080p/30 MP4 = $0.015

Resolution: 720p ×0.5, 1080p ×1, 4K ×4. Format: MP4/WebM ×1, GIF ×1.5, animated SVG ×0.25. PNG, PDF and the widget formats (widgetHtml, widget, project) are one build: 1 credit at any resolution. Publishing a render is free. Events a published widget collects are billed to the widget's owner at $0.01 per thousand. Transcripts and image edits are not metered today. A platform voiceover is 3x Google's rate on the day (about $0.04 a minute of speech now); a voiceover or sound effect on your own vendor key is a $0.002 platform fee.

See the full table + calculator on the pricing page.

Start here: install the render skill

One file with the whole render contract — auth, the estimate → submit → poll flow, the scene geometry rules, costs and error codes. Written for agents; no account needed to read it.

mkdir -p ~/.claude/skills/pinepaper-cloud-render
curl -o ~/.claude/skills/pinepaper-cloud-render/SKILL.md   https://cloud.pinepaper.studio/skills/pinepaper-cloud-render/SKILL.md

Or read it in the browser: /skills/pinepaper-cloud-render/SKILL.md

REST endpoints (/v1)

Endpoint
POST /v1/renders:estimatelive · free exact credit + USD cost
POST /v1/renders:batchEstimatelive · free a whole feed quoted at once: { items: [ …:estimate bodies, each with an optional ref ] } → every row's price and the total (up to 500 rows)
POST /v1/renders:batchChecklive · free does each row of a feed fit its template? { template, rows: [{ ref, values }] } → per field: ok, tight, overflow, cut, no_glyphs, missing or refused, and the estimateItem for :batchEstimate (up to 500 rows)
GET /v1/render-batches/{batchId}live · free every render submitted with batch: { id, ref }: status, what it cost, the artifact URL, what was dropped and the row's fit — to reconcile a feed
POST /v1/scenes:validatelive · free schema + limit diagnostics
POST /v1/renderslive submit — MP4, WebM, GIF, SVG, PNG, PDF and widgets, billed per the estimate
GET /v1/renders/{id}live job status
POST /v1/renders/{id}/cancellive · free stop a render still encoding: its hold is reversed (a reversal on the ledger, no charge) and the container job stopped; a late result is discarded. Repeating it answers already_cancelled; a finished render answers 409
GET /v1/renders/{id}/artifactlive the rendered artifact
GET /v1/renders/{id}/requestlive the retained inputs — scene, export profile, claimed source
POST /v1/assetslive upload a PNG, JPEG, MP4, WebM, WAV, MP3, M4A or FLAC (up to 20 files per multipart request), with an optional licence; returns the ref to use in a scene or a soundtrack
POST /v1/assets/{id}/editlive crop, reframe around the people in the photo, resize and colour-correct an uploaded image — saved as a new asset, original kept
POST /v1/assets/{id}/transcriptlive what an uploaded clip or recording says, as timed words — plus ready caption items, optionally translated
POST /v1/assets/cuts-transcriptlive transcribe a sequence of video cuts and offset timestamps across the cut timeline, returning timed words and captions
POST /v1/voicelive text → a spoken voiceover asset (Gemini TTS, 30 voices, one or two speakers), or on your own ElevenLabs / OpenAI / Cartesia key
POST /v1/voice:estimatelive · free the most a voiceover will cost
GET /v1/voice/keyslive your organisation's voice-vendor keys, masked
PUT /v1/voice/keys/{provider}live store your organisation's ElevenLabs, OpenAI or Cartesia key, encrypted
DELETE /v1/voice/keys/{provider}live remove a stored voice-vendor key
POST /v1/sfxlive a sound effect from a description, on your ElevenLabs key
GET /v1/renders/{id}/componentslive · free every editable part of what was made, with typed fields
POST /v1/renders/{id}/editlive change any part and get a new render; parts you did not name come through unchanged
POST /v1/renders/{id}/publishlive · free put a succeeded render on a public URL and get embed snippets; the same render always gets the same URL
GET /v1/renders/{id}/publish-optionslive · free what a quiz declaration on this artifact may name, before you publish
GET /v1/publishlive · free this organisation's published pages
DELETE /v1/publish/{id}live · free take a page down; its URL then answers 410
GET /v1/publish/{id}/eventslive what viewers did with a published widget
GET /e/{id}public the published artifact — no auth, frameable

Pipelines: a feed of variants

One row of a CSV becomes one version of a template. Every step before the render is free:

  1. POST /v1/renders:batchCheck — each row's values against the template's measured limits, field by field: a headline that would shrink to a caption, a venue that runs off the frame, characters the renderer has no font for. Rows that do not fit are named before anything is reserved.
  2. POST /v1/renders:batchEstimate — the rows' estimateItems priced exactly as :estimate prices them, with the total. It is authoritative; the formula above is the same at any volume (500 six-second 1080p MP4s are 3,000 credits, $45.00).
  3. POST /v1/film-templates/{id}/prepare per row, then POST /v1/renders with an idempotencyKey (a retried submit gets the first render back — same id, idempotent: true — and nothing is held, charged or rendered twice; the same key with a different body is 409) and batch: { id, ref, template, values, expected }. To be told when each finishes instead of polling, set your organisation's webhook once with PUT /v1/render-webhook { url } (its signing secret is shown once) or pass webhook: { url } per render: events render.succeeded, render.failed, render.cancelled, and batch.completed when a batch's expected renders have settled. Failed deliveries are retried with backoff; GET /v1/render-webhook/deliveries lists every attempt.
  4. GET /v1/render-batches/{id} — one row per render to reconcile: status, charge, artifact URL, fit.

Re-running a feed costs only the rows that changed: an identical render comes back cached: true and is not billed. Run pipelines on a service-account key; its own spend cap applies, the daily cap for people does not.

Estimate a render

curl -X POST https://cloud.pinepaper.studio/v1/renders:estimate \
  -H "Authorization: Bearer pp_sa_…" \
  -H "Content-Type: application/json" \
  -d '{"exportProfile":{"durationSeconds":10,"fps":30,"resolution":"1080p","format":"mp4"}}'

# → { "ok": true, "credits": 10, "priceUsd": 0.15, "breakdown": {…} }

Validate a scene

curl -X POST https://cloud.pinepaper.studio/v1/scenes:validate \
  -H "Authorization: Bearer pp_sa_…" \
  -H "Content-Type: application/json" \
  -d '{"scene":{"canvas":{"width":1920,"height":1080},"layers":[…]}}'

# → { "valid": true, "diagnostics": [], "itemCount": 12, "dimensions": {…} }

Render a scene (SVG)

curl -X POST https://cloud.pinepaper.studio/v1/renders \
  -H "Authorization: Bearer pp_sa_…" \
  -H "Content-Type: application/json" \
  -d '{"scene":{…},"exportProfile":{"resolution":"1080p","format":"svg","durationSeconds":5,"fps":30},
       "source":{"provider":"anthropic","model":"claude-sonnet-5","prompt":"a midnight rooftop jazz poster"}}'
# source is REQUIRED: a submit without BOTH source.model and source.prompt returns
# 422 SOURCE_REQUIRED, so every artifact can be traced back to what produced it.

# → { "ok": true, "status": "succeeded", "credits": 2, "priceUsd": 0.03,
#     "cached": false, "statusUrl": "…/v1/renders/rnd_…",
#     "artifactUrl": "…/v1/renders/rnd_…/artifact" }
# Then GET the artifactUrl. Video formats return "encoding" first — poll statusUrl.

Publish an interactive widget

An interactive widget (a scene people move with the cursor, a quiz, an embed) is made by your AI agent, not in the Studio: it is a render whose format is widgetHtml, then a free publish. Over MCP the same two steps are pinepaper_render with exportProfile.format: "widgetHtml" and pinepaper_publish; connect your agent first.

curl -X POST https://cloud.pinepaper.studio/v1/renders \
  -H "Authorization: Bearer pp_sa_…" -H "Content-Type: application/json" \
  -d '{"scene":{…},"exportProfile":{"format":"widgetHtml"},
       "source":{"model":"claude-sonnet-5","prompt":"gears that turn with the cursor"}}'
# → 1 credit at any size. Then:
curl -X POST https://cloud.pinepaper.studio/v1/renders/rnd_…/publish -H "Authorization: Bearer pp_sa_…"
# → { "publishId": "…", "url": "https://cloud.pinepaper.studio/e/…", … }  public, frameable, free

Your footage under the design

Upload a clip, then name it as the scene's baseVideo. The scene's items are drawn over the clip, trimmed from in for the render's duration, and the clip's own sound is kept. MP4 or WebM only; a window past the clip's end is refused, not shortened.

curl -X POST https://cloud.pinepaper.studio/v1/assets -H "Authorization: Bearer pp_sa_…"   -H "Content-Type: video/mp4" --data-binary @clip.mp4
# → { "ref": "/scene-assets/7cd1…", "media": { "durationSeconds": 12.4, "width": 1920, … } }

curl -X POST https://cloud.pinepaper.studio/v1/renders -H "Authorization: Bearer pp_sa_…" -H "Content-Type: application/json"   -d '{"scene":{"canvas":{"width":1280,"height":720},"baseVideo":{"ref":"/scene-assets/7cd1…","in":2},
               "items":[{"type":"text","x":640,"y":620,"content":"Hello","fontSize":48,"fillColor":"#fff"}]},
       "exportProfile":{"format":"mp4","resolution":"720p","fps":30,"durationSeconds":6},
       "source":{"provider":"…","model":"…","prompt":"…"}}'

A clip shorter than the render can fill it with a designed loop: "loop":"pingpong" plays the window from in to out (the clip's end when out is left out; out is read only with loop) forward, then backward, and so on — every frame is a real frame, with no blend and no jump at the turns. "loop":"restart" cuts back to in each time instead. Don't loop by repeating a cut with a crossfade: a crossfade blends the clip's end over its own start, which shows anything that moves twice. A backward pass holds its whole window in memory, so a pingpong window is limited (about 15 s of 30 fps footage in a 1080p render; the refusal names the limit).

"baseVideo":{"ref":"/scene-assets/7cd1…","in":0,"out":5,"loop":"pingpong"}

Or slow it down: "speed" (0.1 to 10; 0.5 is half speed) plays the clip slower or faster, on the single form and on each cut, and the clip's own sound keeps its pitch. "speed":0.4167 (5/12) makes a 5-second clip fill a 12-second render. speed and loop can be used together.

Where the clip sits, in either form: by default it covers the whole canvas, cropped around its middle, so a 16:9 clip under a 9:16 canvas is already a vertical reframe. "focus":{"x":0.2,"y":0.5} (each 0 to 1; 0 is the left or top edge) chooses which part of the shot that crop keeps. "box":{"x":40,"y":120,"width":1000,"height":562} (top-left corner, in the canvas's own units) puts the clip in a rectangle instead — an inset — on the scene's background colour (a hex colour; black otherwise). "fit":"contain" shows the whole clip inside its box or the canvas, with bars of that colour, where "cover" (the default) fills and crops. The scene's items are always drawn on top of the footage.

"canvas":{"width":1080,"height":1920,"backgroundColor":"#0f172a"},
"baseVideo":{"ref":"/scene-assets/7cd1…","loop":"pingpong","box":{"x":40,"y":300,"width":1000,"height":562}}

To cut several pieces together, from one clip or several, give cuts instead. The segments play in order under the design, and each keeps its own sound; a segment with no sound plays silence. Add transition ("crossfade", "fade", "dissolve", "wipeleft", "wiperight", "fadeblack" or {"type":"crossfade","duration":0.5}) to transition seamlessly between cuts. The cuts minus transition overlap must add up to at least the render's durationSeconds, and anything longer is trimmed at the end. Up to 20 cuts. A score on the scene plays over the clip's own sound; "volume" (0–4, default 1) sets the clip's level against it.

"baseVideo":{"cuts":[{"ref":"/scene-assets/7cd1…","in":2,"out":5},
                     {"ref":"/scene-assets/9ab2…","in":0,"out":3,"transition":"crossfade"}]}

Captions — single clip or spliced cut timeline

Ask what a clip says with POST /v1/assets/{id}/transcript, or what an entire sequence of spliced video cuts says across the timeline with POST /v1/assets/cuts-transcript. The answer carries timed words, and with captions it also carries ready caption items (lower-third, bold-center, subtitle, word-highlight or kinetic), each phrase visible only while it is spoken. Add language to translate each phrase while keeping its timing. The same phrases also come back as vtt and srt subtitle files, for players that show a caption track instead of burned-in text. Put the items in a scene whose baseVideo matches the clip or cuts.

curl -X POST https://cloud.pinepaper.studio/v1/assets/cuts-transcript -H "Authorization: Bearer pp_sa_…"   -H "Content-Type: application/json"   -d '{"cuts":[{"ref":"/scene-assets/7cd1…","in":2,"out":5},{"ref":"/scene-assets/9ab2…","in":0,"out":3,"transition":"crossfade"}],
       "captions":{"style":"word-highlight","width":1280,"height":720}}'

Sound — your audio, a voiceover, sound effects

Upload a WAV, MP3, M4A or FLAC like any asset, optionally declaring its rights: x-asset-license is owned (the default), licensed, cc-by (needs x-asset-attribution) or unknown. Unknown rights cannot go into a render. In an MP4 or WebM render, audio: { ref } adds one track and audioTracks mixes up to 8. Each track takes role (voice, music or sfx), offsetSec, inSec, gainDb and fades. A voice track ducks every other track under it while it speaks. The film's provenance lists each sound with its licence, and says when a model made it.

curl -X POST https://cloud.pinepaper.studio/v1/voice -H "Authorization: Bearer pp_sa_…" -H "Content-Type: application/json"   -d '{"text":"Every sound in this film says where it came from.","voice":"Charon","style":"warm, unhurried"}'
# → { "ref": "/scene-assets/72ca…", "seconds": 4.64, "priceUsd": 0.004, "aiGenerated": true,
#     "use": { "audioTracks": [{ "ref": "/scene-assets/72ca…", "role": "voice" }] } }

curl -X POST https://cloud.pinepaper.studio/v1/renders -H "Authorization: Bearer pp_sa_…" -H "Content-Type: application/json"   -d '{"scene":{…},"exportProfile":{"format":"mp4","resolution":"720p","fps":30,"durationSeconds":6},
       "audioTracks":[{"ref":"/scene-assets/bed…","role":"music","gainDb":-8},
                      {"ref":"/scene-assets/72ca…","role":"voice","offsetSec":0.3}],
       "source":{"provider":"…","model":"…","prompt":"…"}}'

Two speakers: {"lines":[{"speaker":"Joe","text":"…"},{"speaker":"Jane","text":"…"}],"speakers":[{"speaker":"Joe","voice":"Puck"},{"speaker":"Jane","voice":"Kore"}]}. The platform voice is priced at 3x Google's rate on the day. That is about $0.04 a minute through 2026 and $0.08 from 2027-01-01 (Google's list rate doubles that day), charged for the seconds spoken and never above the estimate. With "provider":"elevenlabs" | "openai" | "cartesia" the clip runs on the key you stored with PUT /v1/voice/keys/{provider}. That vendor bills your key, we charge a $0.002 platform fee, and the licence follows your plan with that vendor. POST /v1/sfx {"text":"a short cash-register ding","durationSeconds":1.5} makes a sound effect on your ElevenLabs key. Voice clips can be transcribed like any recording, for timed captions.

Edit any part of anything you made

Every render keeps the request that made it, so any part of it can be changed later without remaking the rest. GET /v1/renders/{id}/components lists the parts, each with a path and typed fields:

curl https://cloud.pinepaper.studio/v1/renders/rnd_…/components -H "Authorization: Bearer pp_sa_…"
curl -X POST https://cloud.pinepaper.studio/v1/renders/rnd_…/edit -H "Authorization: Bearer pp_sa_…" -H "Content-Type: application/json"   -d '{"edits":[{"path":"item:caption","set":{"content":"New line"}},{"path":"audio:0","set":{"gainDb":-8}},
               {"path":"entity:sun","set":{"color":"#22d3ee"}},{"path":"image:photo","set":{"adjust.contrast":0.2}}],
       "estimateOnly":true}'
# → the price first; send it again without estimateOnly to render. The new render records derivedFrom.

An edit changes a setting that exists; it never invents structure, and a wrong type is refused by name. A path drawn at its own coordinates (no x/y) lists move.x and move.y: {"path":"item:<id>","set":{"move.x":40,"move.y":-20}} shifts it by that many canvas pixels. The same parts open in Cloud Studio at /studio?render=<id>, and agents use the MCP tool pinepaper_edit_render.

Image edits

Edit an uploaded PNG or JPEG. focalCrop reframes to a new aspect ratio and keeps the detected people (or the main object) in frame. Without a detection it crops to the centre and says "focus":{"by":"centre"}. crop takes exact pixels instead. adjust takes brightness, contrast and saturation (−1…1), grayscale, sepia and vignette (0…1), hueRotateDeg and blurPx. The result is stored as a new asset, and its ref can go straight into a scene. The original is never overwritten.

curl -X POST https://cloud.pinepaper.studio/v1/assets/7cd1…/edit -H "Authorization: Bearer pp_sa_…"   -H "Content-Type: application/json"   -d '{"focalCrop":{"width":1080,"height":1920},"adjust":{"contrast":0.15,"vignette":0.4},"format":"jpeg"}'
# → { "assetId": "9ab2…", "ref": "/scene-assets/9ab2…", "from": "7cd1…", "width": 1080, "height": 1920,
#     "applied": { "crop": {…}, "resize": {…}, "adjust": {…}, "focus": { "by": "detection", "label": "person" } } }

Record what produced the design

Send an optional source block with a render and it is stored with the job and returned on the status endpoint, the retained request, and the user's Renders tab. The render API receives a finished scene, so the model and the instruction behind it are only knowable if you say so — without it, a delivered artifact has no history.

curl -X POST https://cloud.pinepaper.studio/v1/renders   -H "Authorization: Bearer pp_sa_…"   -H "Content-Type: application/json"   -d '{"scene":{…},"exportProfile":{…},
       "source":{"prompt":"a midnight rooftop jazz poster",
                 "provider":"openrouter","model":"anthropic/claude-sonnet-5",
                 "templateId":"tpl_42","conversationId":"conv_9",
                 "app":"pinepaper-studio@1.4.0"}}'

# The inputs come back whole:
curl https://cloud.pinepaper.studio/v1/renders/rnd_…/request -H "Authorization: Bearer pp_sa_…"
# → { "version": 1, "scene": {…}, "exportProfile": {…}, "source": {…},
#     "frames": { "source": "engine", "count": 60, "retained": false } }

Errors use one diagnostic vocabulary everywhere — { code, severity, target, fix } — plus service codes INSUFFICIENT_CREDITS, LIMIT_EXCEEDED.

MCP tools

The same capabilities are exposed as MCP tools. Three doors serve one tool list: https://cloud.pinepaper.studio/mcp (MCP Streamable HTTP) and https://cloud.pinepaper.studio/rpc (JSON-RPC) take a service-account key with the mcp:invoke scope; https://cloud.pinepaper.studio/mcp/v1 takes an OAuth 2.1 token, for Claude Desktop and other MCP clients. tools/list returns the full set; the cloud-specific ones:

Tool
pinepaper_estimatelive · free
pinepaper_validatelive · free
pinepaper_renderlive every render format
pinepaper_edit_renderlive list the parts of any render and change any of them
pinepaper_voicelive a voiceover asset — platform voice, or your own vendor key
pinepaper_sfxlive a sound effect on your ElevenLabs key
pinepaper_publishlive · free put a succeeded render on a public URL, optionally as a quiz
pinepaper_unpublishlive · free take a published page down
pinepaper_publishedlive · free every page this account has published, with its funnel
pinepaper_kg_lookuplive · free what the design graph knows about a concept, relation or format
pinepaper_kg_cataloguelive · free every concept the renderer can actually draw
pinepaper_kg_capabilitylive · free whether a capability can run here, and what it costs
pinepaper_kg_alignlive · free what a format will carry or drop of a concept, before you pay to render it
{
  "mcpServers": {
    "pinepaper-cloud": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://cloud.pinepaper.studio/mcp/v1"]
    }
  }
}

See also: pricing · status · security · general docs.