Lottie Animation Preview & Validator (PNG, GIF, Bodymovin)
Pricing
from $10.00 / 1,000 animation analyzeds
Lottie Animation Preview & Validator (PNG, GIF, Bodymovin)
Use when an AI coding agent or a developer must see and check a Lottie animation (Bodymovin JSON or .lottie): PNG keyframes, contact sheet, short GIF, plus a structured report of errors and features unsupported on iOS, Android or web. No browser. $0.01 per animation.
Pricing
from $10.00 / 1,000 animation analyzeds
Rating
0.0
(0)
Developer
Djam
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
19 days ago
Last modified
Categories
Share
See your Lottie animation and the features known not to work on iOS, Android and web: PNG key frames, a GIF and a machine-readable error report, from a Lottie JSON or a dotLottie file, with no browser. At $0.01 per animation, and nothing for failures.
Built for AI coding agents that write or edit Lottie (Claude Code, Codex, Cursor, through the Apify MCP server), for designers who hand over After Effects exports, and for mobile and web developers who ship them. A model cannot see an animation in a JSON file. This Actor shows it, and says what is wrong.
Give it a Lottie animation (Bodymovin JSON exported from After Effects, or a dotLottie .lottie file), inline or by URL. For each one it returns:
- a contact sheet (one PNG with the key frames: 0 %, 25 %, 50 %, 75 %, 100 % by default) and each key frame as its own PNG: Lottie to PNG;
- a short GIF of the whole animation: Lottie to GIF;
- a structured report a model can read, which makes this a Lottie validator, a Lottie JSON validator an AI agent can act on: size, frame rate, duration, number of layers, errors and warnings with a stable code, the JSON path and the layer name, conformance to the official
lottie-specJSON Schema, and the features known not to work in lottie-web, lottie-android or lottie-ios. It reads any JSON animation exported by Bodymovin; when the renderer cannot draw a file (rare, see Limits), the row still has the report.
It is a Lottie preview for the loop of an agent or a developer: write a Lottie, see it, fix it. It needs no browser and no GPU. One action, one row per animation.
Quick start
Minimal input (an animation pasted inline; everything else has a default):
{"lottieJson": [{"v": "5.7.0", "fr": 30, "ip": 0, "op": 60, "w": 200, "h": 200, "layers": []}]}
Or by URL:
{"urls": ["https://example.com/animation.json"]}
Minimal output, one row per animation (the full real example is in the Output section below):
{"index": 0, "status": "ok", "durationSeconds": 2.0, "summary": {"errors": 0, "warnings": 1, "infos": 0}, "issues": [{"severity": "warning", "code": "ALL_FRAMES_BLANK"}], "compatibility": {"web": [], "android": [], "ios": []}, "preview": {"sheetUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0-sheet.png", "gifUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0.gif"}}
(Real run, shortened: this empty animation draws nothing, so the report says so. A row has more fields, and preview.frames lists each key frame.)
The images are drawn by ThorVG, a Lottie renderer that is close to lottie-web but not identical to the pixel. Use the preview to check what is drawn and where, and the report to know what other players will ignore.
When to use it
- An AI agent just wrote or edited a Lottie JSON and needs to look at the result before it answers: call the Actor with
lottieJson, download the contact sheet, open the image, fix the JSON fromissues. - Check a file before shipping it to iOS, Android and web: the
compatibilityfield lists, per player, the features it ignores (expressions, merge paths, luma mattes, layer effects, some mask modes, text on a path...). - Find why an animation is blank or wrong: empty layers, layers with opacity 0, layers outside their time range, missing assets or parents, a precomposition that contains itself, invalid keyframes, external images that are not embedded.
- Make a GIF or PNG thumbnail of a Lottie or dotLottie file for a README, a ticket or a review.
It does not fix the file: the report says what to fix, a model or a person does it.
For designers: from an export to a preview
You exported a .json (Bodymovin) or a .lottie and want to see it, with the compatibility report for iOS, Android and web?
- A
.jsonfile: open it in a text editor, copy all of it and paste it in Lottie JSON (inline) (up to 5 MB). Then remove the example URL from Animation URLs: it is prefilled, so otherwisebodymovin.jsonis analyzed too, as row 0, and billed. - A file that is online: give its public URL in Animation URLs (up to 10 MB), for example the raw link of a file in a repository or a public share link that downloads the file itself. This is the only way for a
.lottie.
Run the Actor, open the contact sheet and the GIF from the output tab, and read compatibility for iOS, Android and web.
The agent loop
- Call the Actor (
call-actoron the Apify MCP server, or the API) with the animation:lottieJsonfor a file you just wrote,urlsfor one that is online. - Read the dataset row:
status, thenissues(errors first) andcompatibility. - Download
preview.sheetUrland open the image, for examplecurl -o sheet.png "<sheetUrl>". The key frames (preview.frames[].url) and the GIF (preview.gifUrl) are public links too. - Correct the JSON from
issues(each one has acode,pathsas JSON pointers,layerNamesand amessagethat says what to do) and call again.
{"actor": "lakadev/lottie-animation-preview-validator","input": {"lottieJson": [{"v": "5.7.0", "fr": 30, "ip": 0, "op": 60, "w": 200, "h": 200, "layers": []}], "frames": [0, 50, 100]}}
For MCP calls, keep to 5 animations or fewer per call: call-actor waits at most about 45 seconds. If it times out, the run continues; read its result with the run identifier. Running an Actor requires an Apify token (or an x402 or Skyfire payment where available).
Price
Pay per event, no platform usage billed on top:
| Event | Price | When |
|---|---|---|
| Animation analyzed | $0.01 | the animation was read and rendered (row status ok) |
| Actor start | $0.00005 | once per run (Apify's standard start event) |
Nothing is charged for render_failed (the animation was read and reported but no image could be produced, even when the defect is in your file), invalid_input, fetch_failed, robots_disallowed, too_large, invalid_file or not_lottie. If your maximum cost per run is reached, the run stops cleanly at the first row it can no longer pay for: the rows already written are kept.
Input
| Field | Type | Default | Description |
|---|---|---|---|
urls | array of strings | none (the form is prefilled with the bodymovin.json example) | Public http/https URLs of a .json Lottie or a .lottie (dotLottie), up to 10 MB each. |
lottieJson | array | [] | Animations pasted inline, each an object or the JSON text. Up to 5 MB each; use urls above that. |
frames | array of integers | [0, 25, 50, 75, 100] | 1 to 9 percentages of the duration, 0 to 100. |
size | integer | 512 | Longest side of each key frame, 64 to 1024 px. |
gif | boolean | true | Also make the GIF. |
gifSize | integer | 320 | Longest side of the GIF, 64 to 480 px. |
background | string | #FFFFFF | #RRGGBB or transparent (PNG with alpha; the GIF has no transparency and uses white). |
At least one animation in urls or lottieJson, at most 20 in total. The output has one row per input item, in order (urls first, then lottieJson), duplicates included. An invalid input (more than 20 animations, frames out of range, size out of bounds, a bad background...) fails the run with a clear message, before anything is written or charged.
The file type is detected from the content, not the extension: a ZIP is read as dotLottie (versions 1 and 2), anything else as JSON.
{"urls": ["https://raw.githubusercontent.com/airbnb/lottie-web/bede03d25d232826e0c9dca1733d542d8a7754fb/test/animations/bodymovin.json"], "frames": [0, 50, 100], "size": 512, "background": "#FFFFFF"}
Output
One row per animation in the default dataset; the PNG and GIF files are in the default key-value store (anim-<index>-sheet.png, anim-<index>-f<percent>.png, anim-<index>.gif). Real run of the example above (shortened: 2 of 3 frames, paths and layer names cut to 2).
{"index": 0,"source": "https://raw.githubusercontent.com/airbnb/lottie-web/bede03d25d...d8a7754fb/test/animations/bodymovin.json","sourceType": "url","format": "json","status": "ok","error": null,"name": "Bm_fontAnim","lottieVersion": "5.9.6","width": 1820,"height": 275,"frameRate": 30,"inPoint": 0,"outPoint": 103,"totalFrames": 103,"durationSeconds": 3.433,"sizeBytes": 1102932,"counts": {"layers": 332,"precompositions": 23,"imageAssets": 0,"textLayers": 0,"fonts": 0,"expressions": 0,"effects": 0,"masks": 3},"specValid": true,"summary": {"errors": 0,"warnings": 1,"infos": 2},"issues": [{"severity": "warning","code": "UNSUPPORTED_FEATURE","feature": "merge-paths","unsupportedIn": ["ios","web"],"count": 11,"paths": ["/assets/5/layers/12/shapes/0/it/2","/assets/5/layers/13/shapes/0/it/2"],"layerNames": ["boca","O2original 2"],"message": "Merge paths are ignored by lottie-ios and lottie-web; lottie-android draws them only when the app enables merge paths. Flatten the shapes instead. First seen in layer \"boca\" (/assets/5/layers/12/shapes/0/it/2). (11 occurrences in total.)"},{"severity": "info","code": "LARGE_FILE","feature": null,"unsupportedIn": [],"count": 1,"paths": [""],"layerNames": [null],"message": "The animation JSON is 1102932 bytes (over 1 MB); it is heavy to download and parse on mobile. Simplify shapes or compress images."},{"severity": "info","code": "MANY_LAYERS","feature": null,"unsupportedIn": [],"count": 1,"paths": [""],"layerNames": [null],"message": "The animation has 332 layers (over 100); it may be slow on mobile. Merge or remove layers."}],"issuesTruncated": false,"compatibility": {"web": ["merge-paths"],"android": [],"ios": ["merge-paths"]},"preview": {"renderer": "ThorVG 1.0.4","background": "#FFFFFF","sheetUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0-sheet.png","gifUrl": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0.gif","frames": [{"percent": 0,"frame": 0.0,"timeSeconds": 0.0,"width": 512,"height": 77,"blank": false,"url": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0-f0.png"},{"percent": 50,"frame": 51.0,"timeSeconds": 1.7,"width": 512,"height": 77,"blank": false,"url": "https://api.apify.com/v2/key-value-stores/<id>/records/anim-0-f50.png"}]}}
Status of a row
status | Meaning | Billed |
|---|---|---|
ok | read and rendered, all requested images written | yes |
render_failed | read and reported, but no image: the renderer refused the file, crashed or timed out (30 s), or a blocking error (PRECOMP_CYCLE) stopped it | no |
invalid_input | the urls item is not a valid http/https URL (no credentials), or the lottieJson item is neither an object nor a text | no |
fetch_failed | resolution, connection, TLS or time-out, final HTTP status not 2xx, more than 5 redirects, or a non-public address | no |
robots_disallowed | the site's robots.txt forbids the URL | no |
too_large | a limit below was exceeded | no |
invalid_file | neither readable JSON nor a readable dotLottie | no |
not_lottie | JSON that is not a Lottie animation (layers, w, h, fr, ip, op missing or invalid, or op not after ip) | no |
error is null for ok, else one English sentence with the cause. The metadata fields, counts, specValid, compatibility and preview are null when the status is neither ok nor render_failed (preview is also null for render_failed). Key frame p % shows the frame ip + p/100 x (op - ip - 1): 100 % is the last frame. blank is true when the whole image is transparent before the background is applied.
Issues
At most 100 issues per row, one per (code, feature) with a count and the first 5 paths (JSON pointers). Errors first, then warnings, then infos.
| Severity | Code | Meaning |
|---|---|---|
| error | LAYER_MISSING_FIELD | a layer lacks ty, ip, op or a field its type needs (ks, refId, sc/sw/sh, shapes, t) |
| error | MASK_MISSING_FIELD | a mask has no path pt (the renderer can crash on it) |
| error | MISSING_ASSET | a precomposition or image layer refers to an asset that does not exist |
| error | MISSING_PARENT | a layer's parent matches no layer ind |
| error | PRECOMP_CYCLE | a precomposition contains itself, directly or not (no preview) |
| error | INVALID_KEYFRAMES | animated property (a = 1) whose keyframes are empty, malformed, or not in increasing time |
| error | RENDER_FAILED | the renderer refused the file, crashed or timed out |
| warning | UNSUPPORTED_FEATURE | a feature of the table below; feature and unsupportedIn say which and where |
| warning | EXTERNAL_IMAGE | image asset that is not a data: URL (not downloaded: the preview omits it) |
| warning | LARGE_IMAGE | embedded image of more than 16 million pixels |
| warning | FONT_NOT_EMBEDDED | a font used by a text layer has neither an embedded fPath (data: URL) nor glyphs in chars; the preview draws the text with DejaVu Sans instead |
| warning | LAYER_EMPTY | shape layer with no geometry or no style |
| warning | LAYER_INVISIBLE | layer that draws content but has a fixed opacity of 0 (null layers and layers used as a parent are not reported) |
| warning | LAYER_OUT_OF_TIME | top-level layer never shown (outside the animation's ip/op) |
| warning | LAYER_OUT_OF_FRAME | named top-level layer drawn entirely outside the canvas at every sampled frame where it is active: the key frames and 12 frames spread over the animation (only layers with a unique name are judged) |
| warning | UNKNOWN_LAYER_TYPE | ty is not an integer from 0 to 15 |
| warning | DUPLICATE_LAYER_INDEX | two layers of a list share the same ind |
| warning | ALL_FRAMES_BLANK | every key frame is empty |
| info | LAYER_HIDDEN | layer with hd = true |
| info | SPEC_NONCONFORMANT | the document does not conform to lottie-spec 1.0.1; paths gives the first 5 layers that fail the layer schema and count how many (path "" when the layers are fine and the problem is elsewhere) |
| info | LARGE_FILE | animation over 1 MB |
| info | MANY_LAYERS | more than 100 layers |
| info | HIGH_FRAME_RATE | frame rate over 60 |
| info | LONG_ANIMATION | duration over 30 s |
specValid is a yes/no answer from the official JSON Schema: many older Bodymovin 4.x exports are non-conformant but play everywhere, so the Actor does not list schema errors. Its own rules above give the useful ones.
Compatibility table (UNSUPPORTED_FEATURE)
Source: airbnb/lottie supported-features.md, commit a84a05f of 25 March 2026 (MIT). Only the cells marked unsupported are used; unknown cells are not asserted. web means unsupported in all three web renderers (SVG, Canvas, HTML); ios means unsupported in both iOS engines (Core Animation and Main Thread). An empty list in compatibility means "no incompatibility known from this table", not "compatible".
feature | Detected as | Not supported in |
|---|---|---|
expressions | a property with a non-empty expression (x) | android, ios |
layer-effects | an effect on a layer other than drop shadow (25) and Gaussian blur (29) | android, ios |
gaussian-blur | Gaussian blur effect (29) | ios |
merge-paths | a merge-paths shape (mm) | ios, web |
auto-orient | layer with ao = 1 | android, ios |
luma-matte | layer with tt = 3 or 4 | android, ios |
mask-intersect | mask mode i | web |
mask-lighten-darken-difference | mask mode l, d or f | android, ios, web |
mask-expansion | mask with an animated or non-zero expansion | android, ios |
text-path | text layer with a path | android, ios |
text-range-selector | text layer with animators or range selectors | android, ios |
text-glyphs | chars (glyph data) present | ios |
lottie-android draws merge paths only when the app enables them. Windows (lottie-windows, no longer maintained) is not covered.
Limits
- Inputs:
http/httpsURLs of up to 10 MB, inline animations up to 5 MB, at most 20 animations per run, at most 5,000 layers and 1,000,000 JSON values per animation (too_largebeyond). Names copied into the report are cut at 200 characters. A dotLottie archive has at most 1,000 entries, an animation of at most 10 MB once unpacked and 40 MB in total; it is read in memory, never written to disk. - dotLottie: only the animation chosen by the manifest (
initial.animation, else the first one) is read; other animations, themes, state machines, slots and fonts of the archive are ignored. Images stored in the archive are embedded before drawing. - External images are never downloaded: the preview omits them and
EXTERNAL_IMAGEsays so. No LottieFiles API is called. - Not supported in this version: Standby mode and an MCP server of its own (call the Actor with
call-actor), Telegram stickers (TGS), SVG, After Effects.aepfiles, MP4/WebM/APNG/animated WebP output, GIFs at real frame rate, images over 1024 px, comparing several renderers or two versions of a file, performance measures on a device, automatic fixing, React Native, Flutter, Skottie or ThorVG compatibility, blend modes (bm), and the list of every schema error. - Text whose font is not embedded is drawn with a built-in DejaVu Sans (and reported), not with the font you meant. Your own fonts cannot be uploaded in this version.
- The rendering engine (ThorVG 1.0.4) can fail on some files (about 1 in 14 in our checks): the row is then
render_failed, with the report, and is not billed. Previews are close to lottie-web but not identical; compare with a real player when it matters. - After 3 render time-outs in one run, the remaining animations get
render_failedwithout being rendered ("skipped after 3 render timeouts in this run"); none is billed. - The GIF has at most 48 images, covers the whole animation, and loops forever.
- The rendering of each animation runs in a separate process with a memory limit and a 30 s time limit, so a file that crashes the renderer or asks for a huge image only fails its own row.
- Memory: 1,024 MB by default (a quarter of a CPU core): about 1 to 2 s per ordinary animation, a few more for heavy ones. Minimum 512 MB.
- The Actor does not run JavaScript and uses no browser or proxy.
Legal and good manners
URLs are fetched with the User-Agent LakadevLottieBot/1.0 (+https://apify.com/lakadev/lottie-animation-preview-validator). The Actor reads and follows the robots.txt of each origin (RFC 9309), including the origin of every redirect, before any file request, never sends cookies, and refuses non-public addresses (loopback, private, link-local, cloud metadata...) at every redirect: it connects to the address it checked. It does not bypass any protection. The content comes from you, who answer for it. No personal data is kept outside the storages of the run.
Licenses and notices
The Actor's code is MIT licensed (LICENSE). Third-party components are listed in THIRD_PARTY_NOTICES.md:
- ThorVG 1.0.4 (MIT), the rendering engine;
- thorvg-python 1.1.3 (LGPL-2.1), the Python binding. It is used unmodified, as a separate package imported dynamically, so that it can be replaced. Source: https://github.com/laggykiller/thorvg-python;
- Lottie Animation Format JSON Schema 1.0.1, Lottie Animation Community (LF Projects), https://github.com/lottie/lottie-spec (Community Specification License 1.0);
- the compatibility table is derived from airbnb/lottie (MIT); the fallback font is DejaVu Sans (Bitstream Vera license).
FAQ
Is the preview exactly what lottie-web shows? No. It is drawn by ThorVG, which is close to lottie-web but not identical to the pixel. Trust it for what is drawn, where and when; trust compatibility for what other players ignore.
It says compatibility is empty. Is my animation compatible? Not proven. The table is short and sourced; it lists what is known to fail, nothing more.
Why is specValid false on a file that plays everywhere? Many older Bodymovin exports do not follow the current specification. That is why it is only an info issue.
Am I charged for failures? No. Only rows with status ok are billed.
Can I send a file larger than 5 MB? Put it online and use urls (up to 10 MB).