---
name: pinepaper-cloud-render
description: Render animated scenes (MP4, WebM, GIF, PNG, SVG) through the PinePaper Cloud render API. Use whenever asked to produce a video, animation, or image from a described scene without a browser.
---

# PinePaper Cloud Render

A REST API that turns a **scene document** into a rendered artifact. No browser,
no MCP client, no Studio install — the real PinePaper engine runs server-side and
returns the file.

Base URL: `https://cloud.pinepaper.studio`
Auth: `Authorization: Bearer $PP_SA_KEY` (a service-account key) on every call.

## The flow

```bash
# 1. Estimate — free, exact, no hold. Never skip: submit charges on delivery.
curl -sX POST "https://cloud.pinepaper.studio/v1/renders:estimate" \
  -H "Authorization: Bearer $PP_SA_KEY" -H "Content-Type: application/json" \
  -d '{"scene":{...},"exportProfile":{"format":"mp4","durationSeconds":10,"fps":30,"resolution":"720p"}}'
# → { "ok": true, "credits": 10, "priceUsd": 0.15, "breakdown": {…} }

# 2. Submit — charges on delivery. SVG/PNG/GIF return finished; MP4/WebM return "encoding".
curl -sX POST "https://cloud.pinepaper.studio/v1/renders" \
  -H "Authorization: Bearer $PP_SA_KEY" -H "Content-Type: application/json" \
  -d '{"scene":{...},"exportProfile":{...},"source":{"model":"…","prompt":"…"}}'
# → { "ok": true, "id": "rnd_…", "status": "encoding", "statusUrl": "…" }

# 3. Poll until status is "succeeded" or "failed" (video only).
curl -s "https://cloud.pinepaper.studio/v1/renders/rnd_…" -H "Authorization: Bearer $PP_SA_KEY"

# 4. Download.
curl -s "https://cloud.pinepaper.studio/v1/renders/rnd_…/artifact" -H "Authorization: Bearer $PP_SA_KEY" -o out.mp4

# 5. Recover the inputs of any past render (scene + profile + source).
curl -s "https://cloud.pinepaper.studio/v1/renders/rnd_…/request" -H "Authorization: Bearer $PP_SA_KEY"
```

Identical (scene + profile) requests are **content-addressed**: a repeat returns
the cached artifact for free (`"cached": true`).

## `source` is required

Every submit must carry `source.model` or `source.provider`. Without one the
API returns **422 `SOURCE_REQUIRED`** and nothing is charged.

```json
"source": {
  "prompt": "the instruction this design was built from",
  "provider": "anthropic", "model": "claude-sonnet-5",
  "templateId": "tpl_42", "conversationId": "conv_9", "app": "my-agent@1.0"
}
```

The API receives a finished scene — the model and instruction behind it exist
nowhere in that payload. This is the only chance to record them, and they are
what makes the artifact reproducible later via `/request`.

## The scene document

```json
{
  "width": 1280, "height": 720, "background": "#0d1b2a",
  "items": [ … ], "relations": [ … ], "generators": [ … ]
}
```

### Geometry contract — read this before authoring items

- **`x`/`y` is the item's CENTRE**, not its top-left corner. A 900×560
  rectangle at `x:640,y:360` on a 1280×720 canvas spans 190→1090, 80→640.
- Coordinates are **canvas pixels**, origin top-left, y down.
- Anything outside `0..width` / `0..height` is off-frame. Keep text
  comfortably inside: a label at `y:700` on a 720-high canvas is half cut off.
- `resolution` scales the OUTPUT, never the layout: the scene's **short edge**
  is scaled to the tier (720p → 720 lines) and aspect is always preserved.

### Item properties

| Field | Meaning |
|---|---|
| `type` | text, circle, rectangle, ellipse, star, triangle, polygon, pentagon, hexagon, diamond, line, arc, arrow, heart, path |
| `x`, `y` | Centre point (see above) |
| `width`/`height`, `radius` | Size. Stars: `radius` (outer) + `innerRadius` + `starPoints` |
| `content` | Text content (`type: "text"`) |
| `fontSize`, `fontWeight`, `alignment` | Text styling. `alignment`: left/center/right — the anchor shifts with it |
| `fillColor` | Fill. **`"none"` means no fill** — use it for outline-only shapes |
| `stroke` + `strokeWidth` | Outline. `strokeColor` is accepted as an alias |
| `opacity`, `rotation` | 0–1, degrees |
| `line` endpoints | `x`,`y` → `x2`,`y2` |
| `keyframes` | `[{ time, properties: {…}, easing }]` — animate opacity, x, y, scale, rotation, fillColor |
| `animationDuration` | Length of the keyframe sequence, seconds |
| `animationLoop` | **Set `false` for one-shot entrances** |
| `animationTimeOffset` | Delay before this item's animation starts — how you stagger reveals |
| `animationType` | Simple loops: pulse, rotate, bounce, fade, wobble, slide, typewriter |
| `mask`, `effect`, `filter` | Mask presets, visual effects, GPU filters |

```json
{
  "id": "pitch", "type": "rectangle", "x": 640, "y": 360,
  "width": 900, "height": 560,
  "fillColor": "none", "stroke": "#1b3a2a", "strokeWidth": 3,
  "keyframes": [
    { "time": 0,   "properties": { "opacity": 0 } },
    { "time": 0.6, "properties": { "opacity": 1 }, "easing": "easeOut" }
  ],
  "animationDuration": 0.6, "animationLoop": false
}
```

### Relations

`orbits`, `follows`, `attached_to`, `maintains_distance`, `points_at`,
`mirrors`, `parallax`, `bounds_to`, `camera_animates`, `camera_follows`.

```json
{ "from": "planet", "to": "sun", "type": "orbits", "params": { "radius": 120, "speed": 1 } }
```

Camera relations use the literal key `"camera"` for `from`/`to`.

### Generators

`{ "name": "drawWaves", "params": { … } }` — drawSunburst, drawSunsetScene,
drawGrid, drawWaves, drawCircuit. Applied after the background, before items.

## Export profile

```json
{ "format": "mp4", "durationSeconds": 10, "fps": 30, "resolution": "720p" }
```

| Field | Values |
|---|---|
| `format` | mp4, webm, gif, png, svg |
| `resolution` | 720p, 1080p, 4k — the SHORT edge in pixels; aspect preserved |
| `fps` | 30 or 60 |
| `durationSeconds` | Clip length (stills ignore it) |

Cost: `credits = ceil(duration × fpsFactor × resolutionFactor × formatFactor)`,
billed at $0.015/credit. `/v1/renders:estimate` returns the exact number free —
call it, and never guess.

## Supplying your own frames

You may upload `svgFrames: [ … ]` (one animated SVG per frame) instead of
letting the engine animate the scene. If you do:

- Frames must be **canvas-sized and the same aspect as the scene** — they are
  rasterized to the scene's dimensions, so a mismatched frame is a distorted one.
- GIF: 2–150 frames. MP4/WebM: up to 1800.
- Frame count must equal `durationSeconds × fps`, or playback speed drifts.

Omit `svgFrames` and the engine renders the animation from your keyframes and
relations — that is the path to prefer.

## Pitfalls

1. **`animationLoop` defaults to true.** One-shot entrances need `false`, or
   they replay for the whole clip.
2. **`fillColor: "none"` for outlines.** Omitting the field entirely gives the
   shape a default fill, which reads as a solid block over your composition.
3. **Stagger with `animationTimeOffset`**, not by shifting keyframe times.
4. **Estimate before submitting.** A submit charges on delivery; an estimate is free.
5. **Keep the payload under ~6 KB** if you can — very large scenes can trip the
   edge request limit (error 1102). Shorten ids and text before splitting the scene.
6. **Poll video jobs.** MP4/WebM return `"encoding"`; the artifact URL only
   works once status is `succeeded`.

## Errors

| Code | Meaning |
|---|---|
| 422 `SOURCE_REQUIRED` | No `source.model` / `source.provider` |
| 422 `invalid` + diagnostics | Scene or profile rejected — each diagnostic carries `{ code, severity, target, fix }` |
| 402 `INSUFFICIENT_CREDITS` | Top up; nothing was charged |
| 422 `VIDEO_FRAMES_REQUIRED` / `VIDEO_TOO_MANY_FRAMES` | Frame count outside the allowed range |
