Endpoint reference
Base URL: your backend origin + /api. On the hosted deployment that is
https://motion-back-production.up.railway.app/api.
Conventions
| Auth | Authorization: Bearer <key or JWT>, or X-API-Key: <key> |
| Content type | application/json on every request with a body |
| Pagination | ?limit=&offset= — limit defaults to 50, capped at 100 |
| List envelope | { items, total, limit, offset } |
| Timestamps | ISO 8601, UTC |
| Errors | { statusCode, message, error, requestId, path } |
In the Credential column, session means a signed-in JWT only; key means an API key with the named scope, and a session JWT also works.
API keys
| Method | Path | Credential | Returns |
|---|---|---|---|
GET | /v1/keys | session | Paginated ApiKeySummary[] |
POST | /v1/keys | session | ApiKeySummary + secret |
DELETE | /v1/keys/{id} | session | { "revoked": true } |
These three are session-only by design. An API key calling them gets
403 session_required regardless of its scopes.
Create body
{
"name": "n8n production",
"scopes": ["renders:read", "renders:write", "templates:read", "usage:read"],
"expiresAt": "2027-08-27T00:00:00.000Z"
}name defaults to "API key" and is truncated to 80 characters. scopes
defaults to the four above. expiresAt must be in the future, or omitted.
ApiKeySummary
{
id: string
name: string
prefix: string // "pm_live_ab12cd" — not the secret
scopes: string[]
createdAt: string
lastUsedAt: string | null
requestCount: number
expiresAt: string | null
revokedAt: string | null
}The create response adds secret: string, returned once and never again.
Templates
| Method | Path | Credential | Returns |
|---|---|---|---|
GET | /v1/templates | key · templates:read | Paginated templates |
GET | /v1/templates/{id} | key · templates:read | One template |
POST | /v1/templates | key · templates:write | The created template |
POST | /v1/templates/import | key · templates:write | Template + requiredInputs + warnings |
DELETE | /v1/templates/{id} | key · templates:write | { "deleted": true } |
Import takes multipart/form-data, not JSON:
| Field | Required | Notes |
|---|---|---|
file | yes | The .motion bundle directory, zipped. Legacy single-file projects work unzipped. |
name | no | Defaults to the composition’s name |
description | no | Up to 500 characters |
compId | no | Which composition; defaults to the project’s active one |
includeText | no | "true" also exposes text layers |
Inputs are derived from layer names, and a layer whose source is local becomes required. See Automating a project you already have.
| Error | Meaning |
|---|---|
nothing_to_expose | No media layers, so an automation would have nothing to change |
unrenderable_assets | A layer’s source cannot be fetched and nothing will replace it |
Template
{
id: string
name: string
description: string | null
width: number
height: number
fps: number
durationSeconds: number
inputs: TemplateInput[]
createdAt: string
updatedAt: string
}TemplateInput
{
id: string // /^[a-z][a-zA-Z0-9]{0,63}$/
label: string
kind: 'text' | 'color' | 'number' | 'image' | 'media'
required?: boolean // defaults true for image/media
}Publish body — name, document, inputs, width, height, fps,
durationSeconds required; description and projectId optional. See
Templates & inputs.
Renders
| Method | Path | Credential | Returns |
|---|---|---|---|
POST | /v1/renders | key · renders:write | { jobId, status } |
GET | /v1/renders | key · renders:read | Paginated jobs |
GET | /v1/renders/{id} | key · renders:read | One job |
POST | /v1/renders/{id}/cancel | key · renders:write | { jobId, status } |
GET | /v1/renders/{id}/download | key · renders:read | 302 to the MP4 |
Create body
{
templateId: string
inputs: Record<string, string | number>
output?: {
format?: 'mp4'
width?: number // 64–4096
height?: number // 64–4096, w × h ≤ 16 MP
fps?: number // 1–120
}
callbackUrl?: string // public http(s) only
}Optional header: Idempotency-Key, 8–128 characters from A-Z a-z 0-9 . _ : -.
Job
{
jobId: string
status: 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
progress: number
videoUrl: string | null
error: string | null
createdAt: string
}Assets
Upload bytes, get a URL a render can read. The route that removes S3 from the middle of a workflow.
| Method | Path | Credential | Returns |
|---|---|---|---|
POST | /v1/assets | key · renders:write | { url, bytes, mime, expiresAt } |
multipart/form-data with the file in a file field.
{
url: string // pass straight into a render's `inputs`
bytes: number
mime: string // determined from the bytes, not from what you declared
expiresAt: string // 24 hours out
}Accepted: PNG, JPEG, WebP, GIF, SVG, MP4, WebM, QuickTime, MP3, WAV, M4A. Size
is capped by your plan’s maxUploadBytes; over it is 413 naming the limit.
A declared type that contradicts the actual bytes is 400 — but a generic
application/octet-stream is read as “no type given” and the bytes decide.
Uploads are temporary by design and count against assetProcessingBytes, the
same meter URL ingestion uses. For a permanent asset library, use the editor —
/assets is a separate, session-authenticated surface and is not part of this
API.
Usage
| Method | Path | Credential | Returns |
|---|---|---|---|
GET | /v1/usage | key · usage:read | Current period usage and limits |
Shape in Plans, quotas & limits.
Animation presets
The cloud-synced preset library. Not video templates — see the note.
| Method | Path | Credential | Returns |
|---|---|---|---|
GET | /v1/animations | key · templates:read | Paginated presets |
GET | /v1/animations/{id} | key · templates:read | One preset |
POST | /v1/animations | key · templates:write | Upserted preset |
DELETE | /v1/animations/{id} | key · templates:write | { "deleted": true } |
POST upserts on name: publishing the same name again replaces the data and
increments version rather than creating a second row.
{ "name": "Bouncy fade in", "animationData": { "…": "preset payload" } }Webhook delivery
Not an endpoint you call — one Premation sends. POST to your callbackUrl,
once per terminal transition.
| Header | Value |
|---|---|
X-Premation-Event | render.finished |
X-Premation-Job-Id | The job id |
{
jobId: string
status: 'completed' | 'failed' | 'cancelled'
videoUrl: string | null
error: string | null
renderDurationMs: number | null
}Unsigned. Confirm with GET /v1/renders/{id} before acting — see
Webhooks.
Not public
Everything outside /v1 — /auth, /projects, /assets, /ai, /sync,
/files, /billing, /plugins, /admin — backs the editor, the dashboard and
the operator console. Those routes are session-authenticated, change without
notice, and reject pm_live_ keys. Do not build on them.