Player manifest reference
The manifest is the contract between the design plugin and the player. It declares what to render, how to route between variants for a given viewer, and what happens when a viewer interacts.
It is produced by the After Effects plugin at publish time — you do not normally write one by hand — but understanding it explains what a template can and cannot do.
{
"version": "1.0.0",
"lottie": { "defaultUrl": "https://…/animation.json" }
}
version and lottie are the only required fields.
Content
| Field | Type | Purpose |
|---|---|---|
lottie | object | The animation. defaultUrl plus optional variants |
video | object | Background video. defaultUrl, optional variants, and mute |
audio | array | Audio tracks. Each has id, defaultUrl, optional variants, and primary |
poster | object | Still shown before playback. url plus optional variants |
fonts | array | Vectorized glyph artifacts, merged into the Lottie before render |
fonts
Each entry is a .font.json artifact the player fetches and merges into the animation
before rendering. This is why personalized text renders in the correct typeface with no
runtime webfont dependency — no @font-face, no FOUT, and no reliance on the recipient's
machine having the font.
| Key | Purpose |
|---|---|
family | Font family name as referenced by the Lottie |
fName | Font name used inside the animation |
url | Where to fetch the glyph artifact |
style | Optional style qualifier |
ascent | Optional ascent override |
Personalization
| Field | Type | Purpose |
|---|---|---|
dataSchema | object | Declares the data points the template expects |
computed | array | Values derived from the supplied data before rendering |
dataSchema
Each key describes one data point:
| Key | Purpose |
|---|---|
type | string, number, or boolean |
required | Whether the viewer data must supply it |
label, description | Human-facing text for the backoffice |
fallback | Value used when the data does not supply one |
fallback is what makes a template resilient: a missing data point renders the fallback
rather than failing.
computed
Each entry is { key, expr, outputType? }. The expression is evaluated against the
viewer's data before rendering, and key is written back into the data — so later computed
fields can build on earlier ones.
Available operations include arithmetic and rounding (+, round, floor, ceil),
logic (if, and, or, in), string concatenation (cat), and formatting helpers:
| Operation | Purpose |
|---|---|
formatCLP | Chilean peso currency formatting |
formatNumber | Number formatting |
formatPercent | Percentage formatting |
formatDate | Date formatting (YYYY, MMM, LL, short, long, numeric…) |
This is what lets a template show "you saved $1.234.567 this year" from a raw number, without the client's system having to pre-format anything.
Routing between variants
lottie, video, audio, poster, textTracks and transcript all accept variants,
and they all use the same rule shape:
{
"variants": {
"rules": [
{ "dataKey": "segment", "equals": "premium", "src": "https://…/premium.json" }
]
}
}
A rule matches when the named dataKey in the viewer's data equals the given value. The
matched rule supplies the asset — overlayUrl, videoUrl, audioUrl, posterUrl, src,
or inline text for transcripts. A viewer matching no rule gets the default.
Comparison is strict, so a rule should key off a boolean or numeric computed field
rather than a raw data point: host data often arrives as strings, and "true" does not
equal true. The publisher emits a computed for exactly this reason.
The poster follows its own routing, independent of the video's. A template that routes its video by a data point normally wants the still to follow the same branch — otherwise the image advertises a version of the video the viewer will not be shown — but the two are declared separately, so they can diverge deliberately.
This is how one template serves many audiences: the routing happens per viewer, at view time, without rendering a separate video per segment.
Accessibility
| Field | Type | Purpose |
|---|---|---|
textTracks | array | Timed captions painted over the video |
transcript | object | Full text alternative for the whole video |
These are not the same thing, and the distinction matters for compliance.
textTracks are timed captions — each has id, label, srcLang, src, optional
variants and default. They serve a viewer who can see the video but not hear it.
transcript is the WCAG 1.2.1 / 1.2.8 text alternative: the full text of the video, useful
to a screen reader, which timed captions are not. It carries {dataKey} tokens that are
interpolated with the viewer's resolved data — a transcript has to describe what this
viewer saw — and it supports the same variants routing, so a viewer routed to another
composition reads that composition's text.
The text is inline rather than a separate file. On a 40-variant template the transcripts measured 53 KB raw but 1.7 KB gzipped, because they are ~91% identical — external files would have bought nothing.
Interactions
interactions maps a CTA id to what happens when it is clicked. Keys are matched against
the emitted CTA id, case-insensitively.
| Key | Purpose |
|---|---|
navigate | Opens the URL held in the data point named by linkDataKey, with optional target |
setData | Merges values into the live data, re-resolves computed fields and variant rules, and re-renders in place |
name | Human label, reported as cta_name on the cta.clicked event. Falls back to the CTA id |
question | Groups several CTAs into one survey question |
setData
setData re-renders in place — no navigation, no reload. Values may be literal scalars
or JSON-logic expressions evaluated against live data at click time, which is what enables
flows like a bounded quantity stepper.
The player evaluates each value defensively and keeps the prior data when a value is not evaluable. A malformed value is never rejected at manifest load, because doing so would fail the entire manifest instead of no-op'ing a single key.
When both are present
Both run, in a defined order: the data patch is applied first — so a link the patch itself sets is the one that opens — then the link opens synchronously inside the click, and the re-render is queued behind it. The click autopauses, as any navigation does.
question
Survey answers are authored as independent CTAs, so without question a click is just a
click. Give two CTAs the same question and the player additionally emits
interaction.engaged, with the question as interaction_id / interaction_name and the
CTA's label as interaction_value — which is what lets analytics report a distribution
("62% answered Yes") rather than two unrelated counts.
It is analytics-only and does not affect playback.