Skip to Content
Automation APITemplates & inputs

Templates & inputs

A template is a frozen composition plus a manifest of what may vary. It is the only thing the API renders.

Authoring

Expose a field

In the editor’s Template fields panel, pick a layer and expose the property that changes. Each exposed field gets an id, a label and a kind.

Check the ids

The id is the key your automation sends. The editor slugs it from the layer name and it must satisfy:

/^[a-z][a-zA-Z0-9]{0,63}$/

Starts with a lowercase letter; letters and digits only; at most 64 characters. No underscores, dashes or dots — those look like internal ids and invite someone to paste a layer UUID by mistake. Background Video becomes backgroundVideo; a collision becomes background2.

Publish

Save as Animation Template. Publishing refuses if you are signed out, if no field is exposed, or if any id fails the rule above — the error names the offending ids.

Input kinds

KindJSON typeWritten toRequired by default
textstringText layer contentno
colorstringA colour propertyno
numbernumberA numeric propertyno
imagestring (URL)An image srcyes
mediastring (URL)A video or image srcyes

image and media default to required because a template rendered with a missing picture is not a template rendered wrong — it is a blank frame. A field can override this explicitly with required.

Types are enforced. A number field rejects a numeric string; every other kind rejects a non-string. image and media additionally require a public http(s) URL. Invalid values come back as 400 with a per-field list — nothing is rendered.

URL rules for image and media

Asset URLs are fetched server-side, so they go through an SSRF gate. Rejected:

  • Anything that is not http: or https:
  • localhost, 127.0.0.1, private IPv4 ranges, link-local addresses
  • .localhost, .local and .internal hostnames
  • Raw IPv6 literals
  • Cloud metadata hosts

The check is applied to the URL and to what DNS answers, and again across redirects — a hostname that resolves to a private address is refused even though it looks public.

Assets are downloaded into temporary storage before the render, counted against your asset-processing quota, and capped at the smaller of your plan’s maxUploadBytes and 80 MB.

Only fields whose kind is image or media are ingested. A text field whose value happens to be a URL is left alone — it is a caption, not an asset.

Every media layer needs a durable URL

This is the trap that catches a first template, and it is worth understanding before you publish one.

A media layer stores two things: an assetId naming the library entry, and a src. For footage you imported from your own disk, that src is a blob: URL — it belongs to the editor session that created it and means nothing anywhere else. User library imports are deliberately never uploaded to the cloud: they can be gigabytes of raw footage, so they live in local storage and are re-bound to a fresh object URL every time you open the project.

Publishing captures the document exactly as it stands, blob: srcs included.

Publishing refuses a document whose media cannot be fetched. You get a 400 unrenderable_assets naming each layer and why:

{ "code": "unrenderable_assets", "layers": [ { "layer": "Hero Image", "reason": "its footage was imported from local storage and was never uploaded, so only this machine can resolve it" } ] }

This used to be silent. Publishing succeeded, the render job reported completed, and the layer was simply missing from the MP4 — the renderer fetched the src, got nothing, and carried on. If you have a template published before this check existed, re-publish it to find out.

So every media layer in a template must be one of:

Exposed as an image or media inputYour automation supplies a public URL per render — the intended case
Pointed at a public http(s) URLA fixed background or logo hosted on your own CDN
A cloud asset (/files/…)Uploaded through the editor rather than imported locally

If a layer is meant to stay the same on every render, it still needs a real URL — “fixed” is not the same as “local”.

Checking before you rely on it

Render the template once with real inputs and watch the output. A missing background or a blank picture box is the symptom; the fix is to re-host that asset and republish.

Automating a project you already have

POST /v1/templates/import takes a .motion project and gives you back a template your workflow can drive — no editor, no field authoring.

curl -X POST $BASE/v1/templates/import -H "Authorization: Bearer $PREMATION_API_KEY" -F "file=@MyProject.motion.zip" -F "name=Weekly promo"
{ "id": "tpl_7f8a9b", "name": "Weekly promo", "width": 1080, "height": 1920, "fps": 30, "durationSeconds": 6, "inputs": [ { "id": "backgroundVideo", "label": "Background Video", "kind": "media", "required": false, "default": "https://cdn.example.com/bg.mp4" }, { "id": "heroImage", "label": "Hero Image", "kind": "image", "required": true } ], "requiredInputs": ["heroImage"], "warnings": [ "\"Hero Image\" has no usable source in the project, so it is a required input — every render must supply a URL for it." ] }

That is the whole setup. From here it is POST /v1/renders with { templateId, inputs: { heroImage: "https://…" } }.

Zip the bundle

A .motion project is a directory, and HTTP moves files — so send it as a zip. Compressing the folder the normal way is correct: the importer tolerates the whole bundle sitting under one top-level folder, which is what every “compress this folder” command produces.

zip -r MyProject.motion.zip MyProject.motion/

Legacy single-file .motion projects are accepted as-is, unzipped.

Only the chunk files are read — manifest.json, scene.json, animation.json, timeline.json, meta.json. Asset blobs in the bundle are ignored, because the render service cannot use bytes that live on your disk; that is exactly what the derived inputs are for.

Inputs are derived from layer names

Every layer that shows a source becomes an input, and its id is a slug of the layer nameHero Image becomes heroImage. Never the internal node id: addressing a layer by id would make Premation’s node graph a public contract that no project could ever be refactored out of.

So name your layers before you import. Rename a layer and re-import and the input id changes with it, which is correct — the name is the thing you control.

The layer’s sourceWhat you get
Local (blob:, a file path, empty)A required input — every render must supply a URL
Already a public http(s) URLAn optional input, with that URL as its default

requiredInputs in the response is the list to send on every render. A missing one is a 400 before the job is queued, not a blank frame afterwards.

Form fieldDefaultNotes
fileThe zipped bundle. Required.
nameThe composition’s nameTemplate name
descriptionUp to 500 characters
compIdThe project’s active compositionWhich composition to render
includeTextfalsetrue also exposes every text layer as a text input

Scope: templates:writenot granted by default, so ask for it when you mint the key.

Import derives a manifest; it does not read your mind. A project with no media layers is refused (nothing_to_expose) because there would be nothing for an automation to change. If you want finer control — some layers exposed, some locked, a text field renamed — author the template in the editor and publish it instead. Import is the fast path, not the only one.

Publishing from the editor is still the richer path

The editor lets you choose exactly which layers are exposed, rename inputs, group them and set slot fit policies. Import gives you every media layer under its own name and nothing else. Both produce the same kind of template, and POST /v1/renders cannot tell them apart.

The public manifest

GET /v1/templates/{id} returns the manifest your automation reads:

{ "id": "tpl_7f8a9b", "name": "Anime cooking reaction", "description": "Vertical reaction clip with a swappable character", "width": 1080, "height": 1920, "fps": 30, "durationSeconds": 6.5, "inputs": [ { "id": "character", "label": "Character", "kind": "media", "required": true }, { "id": "headline", "label": "Headline", "kind": "text" }, { "id": "accentColor", "label": "Accent", "kind": "color" } ], "createdAt": "2026-08-20T11:04:00.000Z", "updatedAt": "2026-08-20T11:04:00.000Z" }

The manifest deliberately carries no write targets. Which layer and which property a field writes to lives in the document, never in the public response — an automation should not need to know, and should not be able to depend on, Premation’s node graph. The two are joined server-side at render time.

Publishing over HTTP

POST /v1/templates is session-authenticated by default; an API key needs the non-default templates:write scope.

{ "name": "Anime cooking reaction", "description": "Optional, 500 characters max", "document": { "…": "EditorDocument snapshot" }, "inputs": [ { "id": "character", "label": "Character", "kind": "media", "required": true } ], "width": 1080, "height": 1920, "fps": 30, "durationSeconds": 6.5, "projectId": "optional cloud project id" }
FieldConstraint
nameNon-empty string, truncated to 120 characters
descriptionTruncated to 500 characters
documentA JSON object — the full EditorDocument
width, height> 0, at most 8192
fps> 0, at most 240
durationSeconds> 0, at most 3600

In practice you do not hand-build document. Publish from the editor, which captures the whole project — scene, animation, nested comps, timelines, motion blur, guides and colour management — as one consistent snapshot.

Listing and deleting

GET /v1/templates?limit=50&offset=0 GET /v1/templates/{id} DELETE /v1/templates/{id}

Listing is paginated: limit defaults to 50 and is capped at 100. Every response carries { items, total, limit, offset }. Templates are scoped to the account that published them — another account’s id answers 404, not 403.

What is preserved across a render

Everything you did not expose. Replacing a media field’s URL writes exactly one property — that layer’s src. Position, Scale, Rotation, Opacity, easing curves, effects and parenting are untouched, because they live on different props and the input application only ever writes the one target it resolved.

Animation templates are a different thing

/v1/animations is the cloud-synced animation preset library — reusable keyframe presets that sync between your installs. It shares the /v1 prefix and the templates:* scopes, but it is not what /v1/renders renders. If you are automating video, you want /v1/templates.

Last updated on