Skip to Content
Automation APIQuickstart

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 -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

Last updated on