Animated Imports (animated)
Alpha
The contract may change without notice, and imports created with it are not guaranteed to remain compatible. Not covered by the deprecation policy.
Plan requirement
Available from the Pro plan — the same entitlement that gates generating the corresponding assets.
type: "animated" imports produce animated (video) designs — the source for MP4/GIF/HTML5 outputs. The flow is identical to static imports — Create → Upload Assets → Validate → Poll Status — and the payload is authored in the same space as static: integer-pixel geometry and hex colors.
This page lists every rule that differs from static imports. Everything not mentioned here behaves exactly as documented on Create Import.
Authored like static
Animated designs use integer pixels and hex colors (#RRGGBB / #RRGGBBAA), exactly like static. CMYK tokens and the printer format fields (unit, bleed_size, safe_size) are rejected with unsupported_for_type.
Timeline: the top-level animation object
Every animated import requires a top-level animation object describing the timeline. Each layer animates on that one shared timeline — start_at_s/end_at_s place its span, and its tweens drive the motion within:
{
"name": "Spring Sale",
"type": "animated",
"animation": { "duration": 8.0, "screenshot_at_s": 8.0 },
"formats": [ ... ],
"layers": [ ... ]
}| Field | Type | Required | Description |
|---|---|---|---|
duration | float 1 – 60 (seconds) | Yes | The authoritative timeline length. |
screenshot_at_s | float ≥ 0, ≤ duration | No | The moment the HTML5 backup screenshot is captured — not a fallback duration. Defaults to duration. A value greater than duration → out_of_range at animation.screenshot_at_s. |
Supplying animation (top-level or per-layer) on a static or printer import is rejected with unsupported_for_type at the offending path.
Per-layer animation (optional)
Any layer may carry its own animation block controlling when it appears on the timeline and how it moves:
{
"name": "headline",
"type": "text",
"animation": {
"start_at_s": 0.79,
"end_at_s": 8.0,
"tweens": [
{ "type": "slide", "keyframes": [ { "attr": "left", "data": { "type": "start", "time": 0.79 } } ] }
]
}
}| Field | Type | Default | Description |
|---|---|---|---|
start_at_s | float ≥ 0 | 0 | When the layer appears on the timeline. |
end_at_s | float | animation.duration | When it disappears. Must be ≤ animation.duration and strictly greater than start_at_s → out_of_range at layers[i].animation.end_at_s otherwise. |
tweens | array | — | Motion applied while the layer is on screen. See below. |
A layer without an animation block simply spans the whole timeline.
Tweens
Each entry in tweens is { type, keyframes }:
| Field | Type | Required | Description |
|---|---|---|---|
type | enum | Yes | One of slide, fade, scale, rotate, audioFade, textEffect. At most one tween per type per layer — two slide tweens on one layer → invalid_payload. |
keyframes | array (≥ 2 items) | Yes | The keyframe list. Every tween needs at least 2 keyframes; fewer → invalid_payload. Deliberately loosely typed — see below. |
Tweens are rejected on group layers
A group layer may not carry tweens — animate its members instead. A tween on a group is rejected with unsupported_for_type at layers[i].animation.tweens. (A group's own animation timing — start_at_s / end_at_s — is still allowed.)
animation is one block for the whole design, not one per format
A layer's animation sits on the layer, not under a format key — unlike layout and properties. That matches how designs are actually built: a layer's timeline, its tween types, and its keyframe count, order and easing are identical in every format of a design.
Two things legitimately differ between formats, and they are the two the shared block cannot express:
- the
left/topoffsets inslidekeyframes — a slide travels a different distance in a 300×250 than in a 1200×628; - the scale origin of a
scaletween.
On import, the single animation block is applied to every format, so those per-format offsets become uniform. If you need them to differ, adjust the design in the editor after importing.
GET /designs/{designId}/as-import exports one format's animation and warns only when the shared part — timing, tween types, keyframes — genuinely differs between formats. Differing slide offsets and scale origins are expected and are not reported.
Don't hand-author keyframes — round-trip them
Attribute values, data.type, easing and any engine-specific fields are forwarded to the animation engine verbatim, so hand-authoring a keyframe that moves an element to a precise target is fragile. Build the animation once in the Abyssale editor, export it with GET /designs/{uuid}/as-import, and re-import the returned tweens unchanged — they round-trip exactly, with only the ids regenerated.
A keyframe item looks like this, where attr carries the value(s) the tween animates and data.type is usually start / middle / end:
{ "attr": { "left": 0, "top": 12 }, "data": { "type": "start", "time": 0.79 } }| Tween type | Animates |
|---|---|
slide | left and top |
fade | opacity |
scale | scale |
rotate | angle |
audioFade | volumeEffect |
textEffect | typewriting (0–100 progress) + textEffectType |
The structural rules that are enforced
Keyframe items are otherwise passed through as raw objects, because several keyframe generations coexist across the platform. Tween ids are assigned automatically and are never payload fields.
- Every tween carries at least 2 keyframes (a start and an end) — fewer →
invalid_payloadatlayers[i].animation.tweens[j].keyframes. - A layer declares at most one tween per type — a duplicate
type(e.g. twoslidetweens) →invalid_payloadatlayers[i].animation.tweens[j].type. - Every keyframe item carries a numeric
data.time(seconds) — a missing or non-numericdata.time→invalid_payloadatlayers[i].animation.tweens[j].keyframes[k]. The value is not validated: times are neither range-checked against the timeline nor required to be ordered. - Each tween type must animate its own attribute(s): across a tween's keyframes, the
attrobjects must include the type's attributes (see the mapping below) — aslidewithout bothleftandtopanimates nothing and is rejected withmissing_requiredat…tweens[j].keyframes. (Keyframes from older design exports that carry noattrobject are exempt.) - On a
textEffecttween,attr.textEffectType(when present) must be one ofclassic,softFade,focusIn,riseUp,riseDown— anything else isunknown_enum_valueat the exact keyframe path. Omit it to getclassic. grouplayers carry no tweens at all (unsupported_for_type).
The six animations
Every animation is a tween: { "type": ..., "keyframes": [...] } on the layer's animation.tweens. The keyframes below are real shapes produced by the Abyssale editor — the safest starting point for hand-authoring. ease accepts GSAP easing names (none, power2.out, …) and is optional.
slide — move a layer
Animates the layer's position. Every keyframe carries both left and top (px).
{ "type": "slide", "keyframes": [
{ "attr": { "left": 295, "top": 209 }, "data": { "time": 0, "type": "start" }, "ease": "none" },
{ "attr": { "left": 495, "top": 209 }, "data": { "time": 0.6, "type": "end" }, "ease": "power2.out" }
] }fade — opacity
Animates opacity, 0 (invisible) → 1 (opaque). Fade a layer in by starting at 0.
{ "type": "fade", "keyframes": [
{ "attr": { "opacity": 0 }, "data": { "time": 2.2, "type": "start" }, "ease": "none" },
{ "attr": { "opacity": 1 }, "data": { "time": 2.6, "type": "end" }, "ease": "none" }
] }scale — grow / shrink
Animates scale as a percentage of the layer's authored size (100 = as designed). scale is the only accepted attribute.
{ "type": "scale", "keyframes": [
{ "attr": { "scale": 50 }, "data": { "time": 0, "type": "start" }, "ease": "none" },
{ "attr": { "scale": 100 }, "data": { "time": 1, "type": "end" }, "ease": "power2.out" }
] }Scale origin
By default a layer scales from its own placement. Two optional fields on the tween move the anchor it grows or shrinks from, relative to the layer's own box:
| Field | Values | Description |
|---|---|---|
scale_origin_x | left, center, right | Horizontal anchor. |
scale_origin_y | top, middle, bottom | Vertical anchor. |
{ "type": "scale", "scale_origin_x": "left", "scale_origin_y": "bottom", "keyframes": [
{ "attr": { "scale": 0 }, "data": { "time": 0, "type": "start" } },
{ "attr": { "scale": 100 }, "data": { "time": 1, "type": "end" } }
] }The two travel as a pair. Sending one without the other returns conditional_dependency_missing — the renderer combines the axes into a single origin and applies it only when both are set, so a lone value would be silently ignored. Either field on a tween that is not scale returns unsupported_for_type.
rotate — spin
Animates angle in degrees. Values are not clamped to ±360 — -533 spins one and a half turns counter-clockwise.
{ "type": "rotate", "keyframes": [
{ "attr": { "angle": 0 }, "data": { "time": 0.7, "type": "start" }, "ease": "none" },
{ "attr": { "angle": -533 }, "data": { "time": 1.3, "type": "end" }, "ease": "power2.out" }
] }audioFade — volume ramp
audio (and video) layers only. Animates volumeEffect, 0 (silent) → 1 (the layer's max_volume). Fade music out under a voiceover, or in from silence.
{ "type": "audioFade", "keyframes": [
{ "attr": { "volumeEffect": 0 }, "data": { "time": 0.9, "type": "start" }, "ease": "none" },
{ "attr": { "volumeEffect": 0.87 }, "data": { "time": 1.5, "type": "end" }, "ease": "none" }
] }textEffect — per-character text reveal
text layers. Animates typewriting — the percentage of characters revealed, 0 → 100 — and textEffectType selects how each character appears:
textEffectType | Reveal style |
|---|---|
classic | Characters appear in place (typewriter). Default when omitted. |
softFade | Each character fades in. |
focusIn | Each character sharpens from a blur. |
riseUp | Each character rises into place from below. |
riseDown | Each character drops into place from above. |
{ "type": "textEffect", "keyframes": [
{ "attr": { "typewriting": 0, "textEffectType": "riseUp" }, "data": { "time": 0, "type": "start" }, "ease": "none" },
{ "attr": { "typewriting": 100, "textEffectType": "riseUp" }, "data": { "time": 1, "type": "end" }, "ease": "power2.out" }
] }New layer types: video and audio
Two layer types exist on animated designs only. On static / printer they are rejected (unknown_enum_value). Their properties are documented with every other layer type:
The rest of this section covers what is specific to putting them on a timeline.
Both video and audio layers take a required layout and follow the same flat-vs-keyed shape as every other layer (either shape for single-format, keyed by format name for multi-format). An audio layer has no visual box, but layout is still required — supply a placeholder (e.g. { "x": 0, "y": 0, "width": 1, "height": 1 }); the platform stores audio at zero extent regardless.
Using a media extension (.mp4 / .webm / .mp3 / .wav) on an image or logo layer's src is rejected with unsupported_for_type — use a video / audio layer instead.
Extensions are case-insensitive
HERO.MP4 and hero.mp4 are equally accepted. This is the one place case doesn't matter: enum values remain case-sensitive (unit: "MM" is still unknown_enum_value).
Video layer rules
Two layout constraints apply to video layers:
- Videos must be contiguous in the top-level
layersarray — no non-video layer may sit between two videos. Avideointerleaved with other layer types →invalid_payload. - A video may not be a group member — a
videolayer name inside a group'slayer_idsis rejected withunsupported_for_type.
audio counts as a non-video layer
The contiguity rule counts every non-video layer, audio included. An audio layer sitting between two video layers therefore breaks the run and is rejected with invalid_payload.
Remedy: move audio layers so they sit entirely before or after the video run — array position carries no timing meaning for audio, so this costs nothing. Use the layer's animation.start_at_s / end_at_s to place the track on the timeline.
What animated imports do not control
Several concepts integrators reach for simply don't exist in this contract:
- No loop, fps, or output-quality fields. Output settings are chosen at generation time, not at import — see Asynchronous Generation.
- No entrance / exit properties. An element's appearance and disappearance are expressed as tweens bounded by its
start_at_s/end_at_s. - A source video is neither trimmed nor looped to fit the timeline. If the clip is shorter or longer than the span you gave it, that is what renders — prepare the file at the length you want.
- Keyframe
data.timevalues are not range-checked, ordered, or deduplicated. They are forwarded to the animation engine as authored.
Format dimensions: 1920 × 1200 bounding box
On top of the generic format bounds, every animated format must fit within a 1920 × 1200 bounding box in either orientation:
max(width, height) ≤ 1920 AND min(width, height) ≤ 1200| Format size | Result |
|---|---|
| 1920 × 1080, 1080 × 1920, 1200 × 1200, 300 × 250 | ✅ Accepted |
| 1250 × 1250, 2000 × 500 | ❌ out_of_range |
Violations return out_of_range at formats[i].width or formats[i].height (whichever side exceeds its cap), with a message naming the rule.
Group layers on animated
On an animated group only layer_ids (membership) and the group's per-layer animation are meaningful. The auto-layout knobs auto_layout / direction / alignment / gap are rejected with unsupported_for_type (the editor drops group auto-layout on animated templates, so they are rejected rather than silently ignored). See Create Import for the base group rules.
Asset size limits
Presigned (filename src) caps are per layer type on animated:
| Layer type | Filename src (presigned upload) |
|---|---|
video | 100 MB |
audio | 25 MB |
image / logo | 20 MB |
Presigned uploads are Content-Type-prefix-enforced per layer type: image/* on image and logo, video/* on video, audio/* on audio.
A data: URI src is capped at 2 MB decoded on every design type and every layer type, because it travels inside the JSON request body. The presigned upload flow supports the full per-type cap since files go directly to storage.
Above any cap, host the file and reference it by public https:// URL — URL sources have no size cap and are fetched server-side, with no upload step.
Request example
An 8-second animated banner: a headline that slides in at 0.79 s, over a full-bleed background video.
{
"name": "Spring Sale — Animated",
"type": "animated",
"target": { "kind": "design", "project_uuid": "f0a9d2bc-1c1a-4f0e-9d4d-c7b8c2c8a7e1" },
"animation": { "duration": 8.0, "screenshot_at_s": 4.0 },
"formats": [
{ "name": "story", "width": 1080, "height": 1920, "background_color": "#000000" }
],
"layers": [
{
"name": "backdrop",
"type": "video",
"layout": { "x": 0, "y": 0, "width": 1080, "height": 1920 },
"properties": { "src": "loop.mp4", "muted": true }
},
{
"name": "headline",
"type": "text",
"layout": { "x": 90, "y": 200, "width": 900, "height": 240 },
"properties": {
"payload": "Spring Sale",
"color": "#ffffff",
"font_size": 120,
"text_align": "center"
},
"animation": {
"start_at_s": 0.79,
"end_at_s": 8.0,
"tweens": [
{ "type": "slide", "keyframes": [
{ "attr": "left", "data": { "type": "start", "time": 0.79 } },
{ "attr": "left", "data": { "type": "end", "time": 1.4 } }
] }
]
}
}
]
}All geometry is integer pixels and every color is hex. The loop.mp4 filename (a video layer) yields one uploads[] entry with max_bytes: 100000000 — for a video above 100 MB, use a public https:// URL src (no size cap, no upload step) instead. The format 1080 × 1920 fits the bounding box (max = 1920, min = 1080).
Reading an animated design back
Once the import is DONE:
GET /designs/{uuid}andGET /designs/{uuid}/formats/{format}additionally return the animation model on animated designs: the top-levelanimation(duration,screenshot_at_s), per-elementanimation(start_at_s,end_at_s,tweenswithid,type,keyframes), and video/audio media attributes.GET /designs/{uuid}/as-importround-trips animated designs back into an import payload — tweenids are regenerated, and ananimation_duration_derivedwarning is emitted when the timeline length had to be derived from element timing (older designs with no stored duration).
Related
- Create Import — request/response reference shared by every design type
- Import Status — error and warning code reference
- Export a Design — export an existing animated design as an import payload
- Print Imports · Multi-Page Print Imports — the other type-specific import rules
