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.
Organizations & service accounts
- An Organization is the billing entity — it owns the credit ledger. Every user automatically has a personal org.
- A Service account is an agent identity under an org: a scoped API key with an optional spend cap and expiry. It's a bounded mandate — a human grants an agent the right to spend up to $X on specific operations.
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
| Scope | Grants |
|---|---|
render:create | Submit renders and spend credits on them |
render:read | Read render jobs and fetch artifacts |
scene:validate | Validate a scene without rendering it (free) |
inference:invoke | Generate designs through /v1/inference |
mcp:invoke | Drive the canvas over MCP (/mcp, /rpc) and join collab rooms |
graph:propose | Propose design-graph candidates for admin review — renders each one and spends credits |
scene:edit | Read and edit cloud-editor scenes (/v1/scenes) — $0.001 per call, charged only when it succeeds |
plan:create | Draft and estimate generation plans |
plan:approve | Approve generation plans and commit budgets |
credits:purchase | Start 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:estimate | live · free exact credit + USD cost |
POST /v1/renders:batchEstimate | live · 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:batchCheck | live · 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:validate | live · free schema + limit diagnostics |
POST /v1/renders | live submit — MP4, WebM, GIF, SVG, PNG, PDF and widgets, billed per the estimate |
GET /v1/renders/{id} | live job status |
POST /v1/renders/{id}/cancel | live · 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}/artifact | live the rendered artifact |
GET /v1/renders/{id}/request | live the retained inputs — scene, export profile, claimed source |
POST /v1/assets | live 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}/edit | live 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}/transcript | live what an uploaded clip or recording says, as timed words — plus ready caption items, optionally translated |
POST /v1/assets/cuts-transcript | live transcribe a sequence of video cuts and offset timestamps across the cut timeline, returning timed words and captions |
POST /v1/voice | live text → a spoken voiceover asset (Gemini TTS, 30 voices, one or two speakers), or on your own ElevenLabs / OpenAI / Cartesia key |
POST /v1/voice:estimate | live · free the most a voiceover will cost |
GET /v1/voice/keys | live 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/sfx | live a sound effect from a description, on your ElevenLabs key |
GET /v1/renders/{id}/components | live · free every editable part of what was made, with typed fields |
POST /v1/renders/{id}/edit | live change any part and get a new render; parts you did not name come through unchanged |
POST /v1/renders/{id}/publish | live · 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-options | live · free what a quiz declaration on this artifact may name, before you publish |
GET /v1/publish | live · 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}/events | live 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:
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.POST /v1/renders:batchEstimate— the rows'estimateItems priced exactly as:estimateprices 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).POST /v1/film-templates/{id}/prepareper row, thenPOST /v1/renderswith anidempotencyKey(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) andbatch: { id, ref, template, values, expected }. To be told when each finishes instead of polling, set your organisation's webhook once withPUT /v1/render-webhook { url }(its signing secret is shown once) or passwebhook: { url }per render: eventsrender.succeeded,render.failed,render.cancelled, andbatch.completedwhen a batch'sexpectedrenders have settled. Failed deliveries are retried with backoff;GET /v1/render-webhook/deliverieslists every attempt.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:
- Items (
item:<id>): text, captions, shapes, charts, images — position, size, colour, text, opacity. - Motion (
motion:<id>): each keyframe's time and values. - Relations and generators (
relation:<n>,generator:<n>): their parameters. - Drawn films (
entity:<id>): every declared part's settings, and hide/show. The program is never rewritten. - Footage and sound (
clip:<n>,audio:<n>): in/out points, volume, offset, gain, fades, role. - Edited images (
image:<id>): the edit's own settings. A change is re-applied to the original, never stacked on edited pixels. - Scene settings (
scene,scene:<name>): background, canvas, a 3D world's preset and seed, camera, and any other setting the scene carries. - Output (
output): format, resolution, fps and length.
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_estimate | live · free |
pinepaper_validate | live · free |
pinepaper_render | live every render format |
pinepaper_edit_render | live list the parts of any render and change any of them |
pinepaper_voice | live a voiceover asset — platform voice, or your own vendor key |
pinepaper_sfx | live a sound effect on your ElevenLabs key |
pinepaper_publish | live · free put a succeeded render on a public URL, optionally as a quiz |
pinepaper_unpublish | live · free take a published page down |
pinepaper_published | live · free every page this account has published, with its funnel |
pinepaper_kg_lookup | live · free what the design graph knows about a concept, relation or format |
pinepaper_kg_catalogue | live · free every concept the renderer can actually draw |
pinepaper_kg_capability | live · free whether a capability can run here, and what it costs |
pinepaper_kg_align | live · 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.