Quickstart
Four steps, ending in an MP4 URL. Only the last two repeat.
This page takes the import path: the project goes up as a file and the editor never opens. If you want to choose exactly which layers are exposed, rename inputs or set slot fit policies, jump to authoring in the editor — everything from step 3 on is identical.
Mint an API key
In the editor, open Settings ▸ Developer / API, or call POST /v1/keys
with your session.
{
"name": "n8n production",
"scopes": ["renders:read", "renders:write", "templates:read", "templates:write", "usage:read"],
"expiresAt": "2027-08-27T00:00:00.000Z"
}Ask for templates:write explicitly. It is the one scope not in the
default grant, and importing a project needs it. Without it the import answers
403 insufficient_scope.
The secret is shown exactly once, in the create response. It is stored server-side only as an HMAC hash — nobody, including Premation, can recover it. Copy it into your secret manager before you close the dialog.
Import the project
A .motion project is a directory, so zip it. Compressing the folder the
ordinary way is correct — the importer handles the bundle sitting one level
down, which is what every “compress this folder” command produces.
zip -r MyProject.motion.zip MyProject.motion/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."
]
}Keep the id. requiredInputs is the list you must send on every render —
those are the layers whose artwork lives only on your machine, so the render has
no picture for them unless you supply one.
Input ids come from layer names. Name your layers before importing, and
Hero Image becomes heroImage. See
Templates & inputs.
Get your file to a URL
If the picture you want to drop in is already public, skip this and use the URL. If your workflow is holding bytes, upload them:
curl -X POST $BASE/v1/assets \
-H "Authorization: Bearer $PREMATION_API_KEY" \
-F "file=@new-hero.png"{
"url": "https://res.cloudinary.com/…/a1b2c3.png",
"bytes": 284119,
"mime": "image/png",
"expiresAt": "2026-08-28T09:00:00.000Z"
}The file lives for 24 hours — long enough to render from, not a library.
Queue a render, then collect it
cURL
curl -X POST $BASE/v1/renders \
-H "Authorization: Bearer $PREMATION_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711-render" \
-d '{
"templateId": "tpl_7f8a9b",
"inputs": { "heroImage": "https://res.cloudinary.com/…/a1b2c3.png" },
"callbackUrl": "https://hooks.example.com/premation"
}'The response is immediate — the render has not happened yet:
{ "jobId": "clx9zz110000", "status": "queued" }With a callbackUrl the result comes to you. Without one, poll:
async function waitForRender(jobId, apiKey) {
for (;;) {
const res = await fetch(`${BASE}/v1/renders/${jobId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
})
const job = await res.json()
if (job.status === 'completed') return job.videoUrl
if (job.status === 'failed' || job.status === 'cancelled') {
throw new Error(job.error ?? job.status)
}
await new Promise((r) => setTimeout(r, 3000))
}
}GET /v1/renders/{jobId}/download answers 302 straight to the file, which
is what you want if the next step just needs to fetch bytes.
What “done” looks like
{
"jobId": "clx9zz110000",
"status": "completed",
"progress": 100,
"videoUrl": "https://res.cloudinary.com/…/out.mp4",
"error": null,
"createdAt": "2026-08-27T09:00:00.000Z"
}Authoring in the editor instead
Import exposes every media layer and nothing else. When you want a curated template — some layers swappable, the rest locked, inputs renamed and grouped — author it:
Expose the layers that change
Open the composition and use the Template fields panel. Select a layer, expose the property that varies, and give it an id. You need at least one.
Fix any local media
Publishing refuses a document whose media the renderer cannot fetch, naming
each offending layer. Footage imported from your own disk carries a blob:
source that means nothing off your machine — re-host it, or expose it as a
required input so an automation supplies it.
Publish
Save as Animation Template, signed in to the cloud. You get a template id,
and from there steps 3 and 4 above are identical.
The document snapshot is frozen at publish time — editing the composition afterwards does not change the template. Publish again to update it. The same is true of an imported project: re-import to pick up changes.
Next
- Templates & inputs — import options, input kinds, URL rules.
- Webhooks — stop polling.
- n8n, Zapier & CI — the wiring, node by node.
- Errors — what each refusal means, and which are worth retrying.