Automation API
Send us a project, swap any layer by its name, and render it as many times as you like over HTTP.
curl -X POST https://motion-back-production.up.railway.app/api/v1/renders \
-H "Authorization: Bearer pm_live_…" \
-H "Content-Type: application/json" \
-d '{
"templateId": "tpl_7f8a9b",
"inputs": {
"headline": "Launch Special 2026",
"character": "https://cdn.example.com/photo.jpg"
}
}'{ "jobId": "clx9zz110000", "status": "queued" }The API is a hosted-build feature and is plan-gated. The open-source local
edition has no backend to call, and a free account cannot mint keys —
GET /v1/usage reports limits.apiEnabled: false and key creation answers
403 upgrade_required. See Plans, quotas & limits.
What it is for
The API renders templates — a frozen composition plus a manifest of what may vary. There is no layer CRUD, no keyframe endpoint and no way to build a composition over HTTP; that is the editor’s job. What crosses the wire is a template id and a flat bag of named values.
Getting a template is a one-time step, and there are two ways to do it:
| Upload a project | Author in the editor | |
|---|---|---|
| How | POST /v1/templates/import with a zipped .motion | Expose fields, then Save as Animation Template |
| Inputs | Every media layer, named after the layer | Exactly the ones you choose |
| Control | None — you get what the project has | Rename, group, set slot fit, lock the rest |
| Editor needed | No | Yes |
Both produce the same kind of template, and POST /v1/renders cannot tell them
apart. Import is the fast path; the editor is the precise one.
After that, forever:
POST /v1/renders { templateId, inputs } → { jobId, status }
GET /v1/renders/{jobId} → status, progress, videoUrl
GET /v1/renders/{jobId}/download → 302 to the MP4Everything you did not expose stays exactly as authored. Swapping a media
input’s src does not disturb its Position, Scale, Rotation, Opacity or easing
tracks — that is the whole point of “create once, automate many times”.
Inputs are named after layers
An input id is a slug of the layer name — Hero Image becomes heroImage.
Never the internal node id, in either path. Addressing a layer by id would make
Premation’s node graph a public contract that no project could ever be
refactored out of, so it is not one.
Practically: name your layers before you import.
Files, not just URLs
image and media inputs take a public http(s) URL, and the render service
fetches them itself. When your workflow is holding the bytes rather than a URL —
an n8n form upload, a Drive node, a mail attachment — POST /v1/assets takes
the file and hands back a URL to put straight into inputs. No bucket in the
middle whose only job is to bridge two systems that both already have the file.
Why the output matches your export
The render service runs the same renderOffline path the desktop
File ▸ Export uses, against the same frozen document. It is not a
re-implementation and not a headless browser screenshotting the canvas.
Pixel-identical is best-effort, not guaranteed: the hosted renderer runs on SwiftShader (CPU), your desktop runs on your GPU. Geometry, timing and compositing are identical; a driver-level rounding difference in a gradient or a blur is possible.
The shape of it
Key to first MP4 in five steps.
Quickstartpm_live_ keys, scopes, expiry, revocation.
Importing a .motion, authoring, input kinds and slug rules.
POST /v1/assets — bytes in, renderable URL out.
Create, poll, cancel, download. Idempotency.
RendersOne POST when the job settles, instead of polling.
WebhooksEvery route, verb, scope and response in one table.
Endpoint referenceBase URL
All paths in this section are relative to your backend origin plus /api.
| Deployment | Base |
|---|---|
| Premation hosted | https://motion-back-production.up.railway.app/api |
| Self-hosted | https://<your-motion-back-host>/api |
| Local development | http://localhost:4000/api |
So POST /v1/renders means
POST https://motion-back-production.up.railway.app/api/v1/renders.
The hosted origin is the deployment this documentation is written against. If
you run your own motion-back, substitute your origin everywhere — the paths,
verbs and payloads below are unchanged.
Versioning
Everything public lives under /v1. Routes outside /v1 — /auth,
/projects, /assets, /ai, /sync, /admin — back the editor and the
dashboard, are session-authenticated, and are not a public contract. An API
key cannot call them: any /v1 route without an explicit scope mapping is
session-only by default, and everything outside /v1 rejects pm_live_ keys
outright.