Plans, quotas & limits
The API is plan-gated
API access is a paid feature. Two things follow from that:
POST /v1/keysanswers 403upgrade_requiredon a plan without it.- Every API-key-authenticated request re-checks the plan, so a downgrade takes effect immediately rather than at the next renewal.
GET /v1/usage is the authority, not this page. Quotas are set per
deployment and change with plans; the numbers below describe the hosted
backend at the time of writing. Read them from the API rather than hard-coding
them.
GET /v1/usage
GET /v1/usage
Authorization: Bearer pm_live_…{
"period": "2026-08",
"renderJobs": 42,
"renderDurationMs": 3120450,
"renderedMinutes": 18.4,
"reservedRenderMinutes": 0.5,
"apiRequests": 311,
"assetProcessingBytes": 184320000,
"limits": {
"apiEnabled": true,
"monthlyRenderMinutes": 60,
"monthlyApiRequests": 1000,
"maxActiveApiKeys": 5,
"maxUploadBytes": 104857600
}
}Scope: usage:read.
| Field | Meaning |
|---|---|
period | The UTC billing month, YYYY-MM. Counters reset when it rolls over. |
renderedMinutes | Output duration committed by finished renders |
reservedRenderMinutes | Held by jobs currently queued or processing |
apiRequests | API-key-authenticated requests this period |
assetProcessingBytes | Bytes ingested from image / media inputs |
limits.apiEnabled | Check this first. false means keys will not work. |
Gate your integration on limits.apiEnabled, not on the plan name. It is the
single flag the server itself uses, and it accounts for a lapsed payment as
well as a plan that never included the API.
Quotas on the hosted backend
| Quota | API-enabled plan |
|---|---|
| Render minutes per month | 60 |
| API requests per month | 1,000 |
| Active API keys | 5 |
| Asset ingest per input (by URL) | The smaller of your plan cap (100 MB) and the hard 80 MB fetch limit — so 80 MB |
File upload (POST /v1/assets) | Your plan’s maxUploadBytes — 100 MB |
A quota breach is 403 with a machine-readable code, not a silent truncation:
{
"code": "quota_exceeded",
"kind": "render_minutes",
"limit": 60,
"used": 59.5,
"message": "Monthly render minutes quota reached."
}How render minutes are counted
Minutes are reserved before the render and settled after it — the reserve
is the template’s durationSeconds, converted to minutes.
This is why a job can be refused while renderedMinutes still looks
comfortable: the check is renderedMinutes + reservedRenderMinutes + this job. Reserving up front is what stops two concurrent requests from both
passing the same last-minute check and jointly overrunning the quota.
The reservation is released when the job reaches any terminal state, including
cancelled and failed. A cancelled render costs you nothing.
Note that the reserve is based on the template’s output duration, not on how long the render takes. A 6-second clip that takes four minutes of CPU to produce still costs 0.1 minutes of quota.
Request counting
Only API-key requests are counted against monthlyApiRequests. Editor and
dashboard traffic on a session JWT is not. Every /v1 call with a key counts —
including the polling loop, which is a reason to prefer
webhooks on a tight quota.
Uploaded and ingested bytes cost the same
assetProcessingBytes meters both routes to the same outcome: a file sent to
POST /v1/assets, and an asset fetched from a URL you passed as an input. There
is no cheaper path, deliberately — the choice between them should be about what
your workflow is holding, not about billing.
| Lifetime | 24 hours, then deleted |
| Counted against | assetProcessingBytes |
| Size cap | limits.maxUploadBytes — over it is 413 naming the limit |
| Types | PNG, JPEG, WebP, GIF, SVG, MP4, WebM, QuickTime, MP3, WAV, M4A |
Uploads are temporary because they exist to feed a render. For a permanent
asset library, use the editor — /assets is a separate,
session-authenticated surface and is not part of this API.
Importing a project
POST /v1/templates/import costs one API request and nothing else — no render
minutes, no asset bytes. The bundle’s own asset blobs are never ingested,
because the render service cannot use bytes that live on your disk; that is what
the derived inputs are for.
The template limits below still apply to what the imported composition declares: a project at 8192px or a 90-minute composition is refused.
Rate limits
Separate from the monthly quota, and evaluated per account:
| Limit | Value | Answer |
|---|---|---|
| Render creation | 60 per minute | 429 rate_limit_exceeded |
| All requests | 300 per minute per IP | 429 |
Hard limits
These are not plan-dependent — they apply to every deployment.
| Limit | Value |
|---|---|
| Output format | MP4 (H.264) only |
| Output dimensions | 64–4096 px per side, at most 16 megapixels |
| Output fps | 1–120, integer |
Template width / height | ≤ 8192 |
Template fps | ≤ 240 |
Template durationSeconds | ≤ 3600 |
| Request body | 10 MB |
| Asset fetch | 80 MB |
Template name | 120 characters |
Template description | 500 characters |
| Page size on any list | 100 |
Idempotency-Key | 8–128 URL-safe characters |
Not supported
| Alpha in output | MP4 has none — transparency flattens onto the comp background |
| Audio | Not muxed by the hosted renderer yet |
| Non-MP4 formats | WebM, ProRes, PNG sequence, Lottie, SVG and GIF are desktop-export only |
| Plugin effects | Skipped silently — the hosted renderer has no plugins installed |
| Layer or keyframe CRUD | Not exposed; the API renders templates, it does not edit them |
Reading a template back as a .motion | Import is one-way; there is no export endpoint |