A card template is a single JSON document describing a card's artwork: its size, its sides, the elements painted on each side, and the variables a person fills in. The same file produces the same picture in a browser, on a server, and in a print pipeline.
The TemplateKit Inspector renders a template, shows its layer tree and edits its variables. The file is read in the browser and not uploaded.
Format version 1.x. Only major version
1is supported. The minor version is not read, so1.0and1.5are treated identically.
A minimal template
Only the required fields: a background and one line of text bound to a variable.
{
"format_version": "1.0",
"version": "1.0.0",
"id": "minimal-card",
"name": "Minimal",
"product": "card_cr80",
"width": 1012,
"height": 638,
"fields": {
"type": "object",
"properties": {
"displayName": {
"type": "string",
"title": "Display name",
"default": "You"
}
},
"required": ["displayName"]
},
"template_data": [
{
"name": "front",
"background": {
"id": "front_bg",
"type": "rect",
"properties": { "fill": "#fef3c7" }
},
"elements": [
{
"id": "displayName",
"type": "text",
"pos": { "x": 64, "y": 80 },
"size": { "width": 884, "height": 80 },
"properties": {
"value": "{{displayName}}",
"font": { "family": "Comfortaa", "size": 56, "weight": 700 },
"color": "#1a1a1a",
"align": "left"
}
}
]
}
]
}
Top-level fields
| Field | Required | Meaning |
|---|---|---|
format_version | ✅ | The wire format. Major version must be 1. |
version | ✅ | Your template's own version string. Non-empty; otherwise unconstrained and unread. |
id | ✅ | Stable identifier. Non-empty. |
name | ✅ | Human-readable name. Non-empty. |
product | ✅ | The product the artwork targets, e.g. card_cr80. A free string; the renderer never reads it. |
width, height | ✅ | Canvas size in pixels. Positive integers. |
fields | ✅ | The variables this template accepts. May declare no properties. |
template_data | ✅ | One entry per side. Must not be empty. |
description, mood | — | Free-text metadata. |
author | — | { name, url? }. |
fonts | — | Font declarations. |
variants | — | Named colourways. |
assets | — | Images carried inside the file. |
source | — | Provenance. Only kind is fixed; all other keys are preserved verbatim. |
warnings | — | Advisory export notes. |
Coordinates are bare numbers meaning pixels. There is no unit field, and no
bleed or safe-area concept in this format. The origin is the top-left, y down,
and a nested element's pos is relative to its parent.
Unknown keys are stripped, not rejected. Validation discards keys it doesn't know, everywhere except
sourceand variant element overrides. A validated template is not a faithful round-trip of the input.
Sides
template_data is an array of frames, one per side. Each frame needs a
unique name, a background, and its elements:
"template_data": [
{ "name": "front", "background": { }, "elements": [ ] },
{ "name": "back", "background": { }, "elements": [ ] }
]
front/back is a card convention, not a rule. The format requires at least one
uniquely-named frame.
background is required, and must be a rect or an image (not any element
type). It must cover the whole frame: either omit pos/size, or set pos to
{x: 0, y: 0} and size to the template's width/height.
Elements
Every element has an id and a type. Everything else is optional:
| Property | Meaning |
|---|---|
pos, size | Position and size. Optional; both default to zero. |
rotation, opacity, blendMode, blur, shadow | Visual modifiers. |
layoutChild | How the element behaves inside an auto-layout frame. |
There is no visible or hidden property. A layer with opacity: 0 is
invisible and keeps its place in the layout, so siblings don't reflow.
Element types
type | Paints | Required properties | Also supports |
|---|---|---|---|
text | A run of text | font (with family, size) | value, spans, color, fill, align, verticalAlign, fit, case, leadingTrim, maxLines |
image | A raster | src and fit | cornerRadius, mask, stroke |
rect | A rectangle | none | fill, stroke, cornerRadius, cornerSmoothing |
vector | An SVG path | d | fill, stroke |
qr_code | A QR code | value | errorCorrection (L/M/Q/H), foreground, background, margin |
frame | A container | children | fill, stroke, cornerRadius, clipsContent, layout |
image.fit is one of cover, contain, fill, tile. Font weight is one of
400, 500, 600, 700, 800. lineHeight is a number or "auto" (resolved
per family at render time; a consumer that ignores the marker renders at 1.2).
A text element needs neither value nor spans; with neither it paints an
empty string. When both are present, spans wins.
Fills accept a solid colour or a gradient (linear, radial, or angular), each
gradient with a stops array of { offset, color } (at least two stops).
Nesting and auto-layout
Elements nest only through frame.properties.children. A frame can position
its children absolutely, or lay them out automatically via
frame.properties.layout:
{
"id": "row", "type": "frame",
"pos": { "x": 64, "y": 400 },
"properties": {
"layout": { "direction": "row", "gap": 16, "primaryAlign": "start", "crossAlign": "center" },
"children": [ ]
}
}
layout takes direction (row/column, required), gap, crossGap,
padding, primaryAlign, crossAlign and wrap. Each child can carry
layoutChild with width/height (fixed/hug/fill), grow, align,
absolute, and min/max sizes.
Scaling
Rendering scales the whole template by target width ÷ template width. The target
aspect ratio must match the template's within a small tolerance. A mismatch
makes compiling throw; the artwork is never distorted.
Fields (variables)
fields is a JSON-Schema-shaped object: a fixed subset, not real JSON Schema.
Every field is a string; format is the only type-ish distinction.
"fields": {
"type": "object",
"properties": {
"displayName": { "type": "string", "title": "Display name", "default": "You" },
"accent": { "type": "string", "format": "color" },
"photo": { "type": "string", "format": "image", "x-image-aspect": [1, 1] },
"bio": { "type": "string", "format": "longText", "maxLength": 140 }
},
"required": ["displayName"]
}
| Key | Meaning |
|---|---|
type | Always "string". |
title, description | Labels for the input. |
default | Applied before caller-supplied values. |
format | One of color, url, image, longText. Absent means plain text. |
minLength, maxLength, pattern | Validation hints, checked against supplied values. |
readOnly | Not editable by hand. |
x-source | Who supplies it: user, system, or order. |
x-widget | Names a specific input control. |
x-image-aspect | [w, h] aspect for an image field. |
Referencing a field
Elements interpolate fields with a mustache token whose id is
[a-zA-Z_][a-zA-Z0-9_]*:
"properties": { "value": "Hello, {{displayName}}" }
Substitution walks every string in every nested property, so a token works in
text.value, text.color, rect.fill, image.src, qr_code.value and so on. A
missing value renders as an empty string.
Every reference inside template_data must resolve to a declared field, or
validation fails with unknown_field_reference. The Inspector also flags a
declared field that no element references.
Reference checking covers
template_dataonly. An unknown token inside avariantsoverride passes validation.
System fields
Some fields are supplied by Davi rather than typed by hand. A consumer treats a
field as system-supplied when readOnly is true, x-source is "system", or
its id is card_url, identifier, or begins with $$.
| Field | Meaning |
|---|---|
card_url | The full per-card tap URL, injected per physical card. |
identifier | The bare card id. Deprecated in favour of card_url; older templates embed https://davi.social/c/{{identifier}}. |
$$davi$… | Davi logo configuration ($$davi$logo_enabled, $$davi$logo_placement, $$davi$logo_color). $ is not valid in a mustache id, so these are never interpolated. They are out-of-band configuration carried in fields.properties and read by the renderer. |
The format itself treats all of these as ordinary string fields; only consumers give them meaning.
Variants
A variant is a named colourway that overrides properties without duplicating the template:
"variants": [
{
"id": "amber",
"label": "Amber",
"swatch": "#fef3c7",
"overrides": [
{
"name": "front",
"background": { "id": "front_bg", "type": "rect", "properties": { "fill": "#fde68a" } },
"elements": [ { "id": "displayName", "properties": { "color": "#78350f" } } ]
}
]
}
]
An override targets a frame by name. background is a whole replacement;
elements are {id, properties} shallow-merge deltas, matched by id and
resolved into nested frame children too. Variant ids and labels are required,
ids must be unique, and each override must name a frame that exists (once per
variant).
Inline assets
An image can reference a raster carried inside the template, addressed by content hash:
"assets": [
{ "sha256": "deadbeef…", "base64": "iVBORw0KGgo…", "contentType": "image/png" }
]
An element points at it with src: "asset:deadbeef…". Compiling rewrites that to
a data: URL. Assets are resolved in frame backgrounds, elements, nested frame
children, and variant overrides.
If a reference has no matching bytes the template still validates and still
renders. The layer paints a grey placeholder with a photo glyph and the
renderer reports an image_load_failed warning. (The sha256 is not verified to
be a hash.)
Fonts
fonts declares the families a template uses so a renderer can fetch them. A
descriptor is one of three kinds.
"fonts": [
{ "kind": "google", "family": "Comfortaa", "url": "https://fonts.googleapis.com/css2?family=Comfortaa:wght@700&display=swap" },
{ "kind": "fontsource", "family": "Plus Jakarta Sans", "url": "https://…css" },
{ "kind": "local", "family": "Acme", "files": [ { "weight": 400, "src": "asset:…" } ] }
]
url is required for google and fontsource, and it must point at a CSS
stylesheet, not a font file. local requires files, each with a weight and
src. Families must be non-empty and not repeat.
The block may be partial or absent, so a template can name a family it never declares. What happens then depends on the consumer:
- A bare renderer supplies no bytes for an undeclared family, so its text paints with whatever fallback the canvas has registered.
- The Davi order pipeline uses fonts stored against the template record.
- The Inspector, which only ever sees your file, looks the family up on Google Fonts by name and reports it as guessed.
A family whose stylesheet fails to load is dropped on its own without taking the other families with it.
Warnings
warnings records what an export could not represent faithfully. They are
advisory: a template with error-severity warnings still validates and still
renders.
"warnings": [
{ "severity": "warn", "code": "raster_empty", "message": "…", "nodeId": "logo" }
]
severity is info, warn, or error. code is an open string; the format
defines no vocabulary. The codes the Figma exporter emits today are
missing_pick, variant_structure_mismatch, raster_empty,
raster_unavailable, and qr_layer_hidden; another producer may emit its own.
Validation
validate() returns either { ok: true, value } or { ok: false, errors },
where each error is { path, code, message } and path is a JSON-Pointer-ish
location like /template_data/0/elements/1/id.
Checks include:
| Check | Codes |
|---|---|
format_version major is 1 | missing_format_version, unsupported_format_version |
id, name, version, product present and non-empty | missing_required_field |
width/height are positive integers | invalid_dimension |
template_data non-empty with unique frame names | empty_template_data, duplicate_frame_name |
| The background covers the frame | background_must_fill_frame |
| Element ids present and unique | missing_element_id, duplicate_element_id |
| Variants well-formed | duplicate_variant_id, missing_variant_label, unknown_frame_name, duplicate_override_name |
Every template_data token resolves | unknown_field_reference |
| Gradients valid | gradient_needs_two_stops, invalid_stop_offset, invalid_stop_color |
| The fonts block is well-formed | font_family_required, duplicate_font_family, stylesheet_font_url_required, local_font_files_required |
A structural problem that fails the base parse, such as a missing font url,
surfaces as the catch-all invalid_shape rather than its specific code, because
the shape check runs first.
Element-id uniqueness is per frame, and only over a frame's top-level
elements. The same id in two different frames passes, and so do two nested children of a frame sharing an id.
validateValues() separately checks supplied values against fields, reporting
missing_required_value, invalid_value_type, value_too_long,
value_too_short, and value_pattern_mismatch.
Legacy files
Older files may use a wrapper: the template nested under template, with
assets/source/warnings beside it, keyed by schemaVersion: 1 (a different
field from format_version). Ids are slugged from layer names, so a file can
also carry duplicate element ids. A reader handles both before validating, in
this order:
- Unwrap the legacy bundle. If the parsed object has a
templatekey and aschemaVersionof1, the template is the value oftemplateandassets/source/warningssit beside it. Otherwise the object is already the template. - Make element ids unique. Rename duplicates in document order (
Vector,Vector_2,Vector_3) and rewrite any references that pointed at them.
Both steps are idempotent. Renaming changes the author's ids, so a tool that shows the file to a person should validate as written first, repair only on failure, and report what it renamed.
Rendering
The format is renderer-agnostic: the file plus the field values is everything needed to paint a card. A conforming renderer:
- Validates the template, and refuses an unknown
variantIdor an output aspect ratio that doesn't matchwidth/height. - Resolves the variant and the requested frames into a node tree, substituting field values into text and image slots.
- Collects font bytes for every family the file declares. Text with no loaded face cannot be shaped and does not paint.
- Paints the tree at the output size, scaling from the template's own
width/heightas described in Scaling.
Nothing in that sequence is Davi-specific. The format on this page, including the element types and scaling rules, is the whole contract.
Not specified
The format leaves these open: the warnings.code vocabulary, product's
semantics (never read), source beyond its kind, whether sha256 is a
sha256, and what a minor format_version bump means.
Next
- TemplateKit Inspector: render and debug a template in your browser.
- Core Concepts: how cards, profiles and wallets relate.