Skip to main content

Templates and formats

A template is how a clip is drawn. It comes in variants, which are cuts of the same idea, in versions, which are builds, and in ratios, which are the shapes it can be drawn at. What matters to a developer is that a template publishes the exact document it accepts, so you can generate a client from it rather than guess.

MethodPathPermission
GET/v1/templatestemplates:read
GET/v1/templates/{templateId}/referencetemplates:read
GET/v1/templates/{templateId}/reference/{ratio}templates:read
GET/v1/formats/templatestemplates:read
GET/v1/formats/template/{templateId}templates:read
GET/v1/formats/feed/{feedId}templates:read
GET/v1/formats/feed/{source}/{format}templates:read
GET/v1/cataloguetemplates:read

/v1/catalogue, /v1/formats/templates, /v1/formats/template/{id} and /v1/formats/feed/... are served without authentication as well. What shape a template accepts is a product description, not customer data, and a producer needs it before it has a reason to authenticate. Nothing in them says who uses a template or what has been sent to it.

Ask a template what it accepts​

This is the call to build a producer around.

curl http://localhost:4000/v1/formats/template/tpl_a9c3fdb1b5c9

The answer carries four things: keys, a human-readable description of every input; jsonSchema, the same thing as JSON Schema, for generating a client; example, a worked payload that satisfies it; and ratios, the shapes this build comes in.

{
"templateId": "tpl_a9c3fdb1b5c9",
"templateName": "Template 1",
"variant": "Standard",
"version": "1.0.0",
"ratios": ["9x16", "4x5", "1x1", "16x9", "2x3"],
"keys": {
"format": "reelwire.keys/1",
"summary": "One story in two parts: clipData, which is drawn in the clip (a headline, an optional second line, up to six figures, up to five points, up to three images and a source), and metaData, which is the post's title, captions and hashtags and is never drawn.",
"keys": [
{
"key": "clipData",
"type": "group",
"label": "Clip data",
"required": true,
"keys": [
{ "key": "headline", "type": "text", "label": "Headline", "required": true, "maxLength": 80, "example": "This is the headline of the clip" },
{ "key": "subline", "type": "text", "label": "Second line", "required": false, "maxLength": 120 },
{
"key": "figures",
"type": "list",
"label": "Figures",
"required": false,
"maxItems": 6,
"item": {
"type": "group",
"keys": [
{ "key": "label", "type": "text", "required": true, "maxLength": 24 },
{ "key": "value", "type": "number", "required": true, "decimals": 4 },
{ "key": "unit", "type": "text", "required": false, "maxLength": 8 },
{ "key": "direction", "type": "enum", "required": false }
]
}
}
]
}
]
},
"jsonSchema": { "type": "object", "required": ["clipData"], "properties": {} },
"example": {
"clipData": {
"headline": "This is the headline of the clip",
"subline": "This is a second line that adds a little more detail",
"figures": [{ "label": "Example figure", "value": 1250, "unit": "", "direction": "up" }],
"bullets": ["This is a short supporting point"],
"images": [{ "alt": "Example image, PNG", "url": "media://example-image.png" }],
"source": "Example source"
},
"metaData": {
"title": "This is the title of the post",
"captionShort": "This is the short caption, for Telegram.",
"captionMedium": "This is the medium caption, for Instagram, TikTok, Facebook Reels and LinkedIn.",
"captionLong": "This is the long caption, for YouTube and Facebook Feed.",
"hashtags": ["example", "reelwire"]
},
"locales": {
"de": { "clipData": { "headline": "Das ist die Überschrift des Clips" }, "metaData": { "title": "Das ist der Titel des Beitrags" } }
}
}
}

Two halves, always. clipData is drawn in the clip. metaData is the post's title, captions and hashtags, and is never drawn on the video.

locales inside the content is how one event carries several languages. Each channel takes its own language's copy laid over the default, so a language with no copy still renders, with the default. The captions come in three lengths because the platforms do: short for Telegram, medium for Instagram, TikTok, Facebook Reels and LinkedIn, long for YouTube and the Facebook feed.

A picture is a file from your media library, and nothing else. Every image field is { "url": "...", "alt": "..." }, and url takes media:// followed by the file's id, or its name as in the example above; the JSON Schema calls the two parts "Picture" and "Alt text". A web address is refused and the refusal names the field: a 400 on a feed, a post list or a direct export, and unmapped with the reason on a stream. Upload the picture to the media library first, then refer to it.

Query it for a particular cut or build with ?variant=Standard&version=1.0.0. Without them you get the first variant and the newest build.

Ask by feed instead​

A producer usually knows it sends central bank decisions, not which template draws those. Which template a feed uses is Reelwire's decision and changes without the producer being told, so ask by the feed and follow.

curl http://localhost:4000/v1/formats/feed/fd_3e7b3bff3194

The answer is the same document, resolved through the feed's current binding. GET /v1/formats/feed/{source}/{format} is the same answer addressed by the two slugs.

What is on the shelf​

GET /v1/formats/templates is every template that can describe itself: one entry per template holding a project with a key mapping, which is exactly the set a producer could usefully be pointed at.

{
"templates": [
{ "templateId": "tpl_a9c3fdb1b5c9", "templateName": "Template 1", "variant": "Standard", "version": "1.0.0", "keys": 2 },
{ "templateId": "tpl_1cbd9f79150f", "templateName": "Template 2", "variant": "Standard", "version": "1.0.0", "keys": 2 }
]
}

A template with no build yet is a name on a shelf with nothing to describe, so it is not listed rather than listed and unusable.

GET /v1/templates is the workspace's own view: what this account is offered, joined with the templates it already meets through a feed, a stream or a post list, each with its inputSchema, duration, ratios and variants.

{
"catalogue": [
{ "id": "tpl_a9c3fdb1b5c9", "name": "Template 1", "family": "", "description": "Template description 1", "variants": ["Standard"] }
],
"templates": [
{
"templateId": "tpl_0083ab80ce80",
"offered": true,
"version": "1.0.0",
"name": "Template 7",
"contentStyle": "motion-graphics",
"family": "Template projects",
"milestone": 1,
"ratios": ["9x16", "4x5", "1x1", "16x9", "2x3"],
"duration": { "mode": "fixed", "frames": 300 },
"subvariants": [],
"overridableStyle": ["*"],
"inputSchema": { "type": "object", "required": ["clipData"] },
"variants": [{ "name": "Standard", "versions": ["1.0.0"] }],
"descriptorSha": "19a9365d778eb833f0f267dad6c17eb3a41a2fc9a5b961f0c784e97a1db84c4f",
"syncedAt": "2026-09-23T18:46:36.864Z"
}
]
}

descriptorSha changes whenever the build behind a version changes, so a client that caches a template can tell when to read it again.

GET /v1/catalogue is the shipped feed catalogue rather than the template shelf: what Reelwire carries, with each format's own payload schema. It is served with an etag and cache-control: public, max-age=30, so a client that has seen the version can skip the parse.

Which builds exist​

curl http://localhost:4000/v1/templates/tpl_a9c3fdb1b5c9/reference \
-H "Authorization: Bearer rw_your_key_here"
{
"templateId": "tpl_a9c3fdb1b5c9",
"name": "Template 1",
"variants": [
{ "name": "Standard", "versions": [{ "version": "1.0.0", "ratios": ["9x16", "4x5", "1x1", "16x9", "2x3"] }] }
]
}

GET /v1/templates/{templateId}/reference/{ratio} returns the reference clip itself, so you can see what a template looks like before you commit to it. It is a video, not JSON:

HTTP/1.1 200 OK
content-type: video/mp4
etag: "5f3346d28eb21a9d24768c82d387c7a66708cf6561511a9f3837c8455f385fa2-9x16"
cache-control: private, no-cache
x-template-version: 1.0.0

Narrow it with ?variant=Standard&version=1.0.0. A ratio that is not NxM is a 400 that says "No such shape".

Versions and variants​

A version names a build. "latest" is a real answer rather than an absence, and it is the default: it follows the newest build, resolved at the moment something renders.

For now every template is at version 1.0.0. An improvement to a template replaces its 1.0.0 build rather than adding a version, so a channel pinned to 1.0.0 and one on "latest" draw the same thing today. The content a template accepts does not change with a new build.

A variant is a cut of the same template. Which variant and which version are the subscribing channel's choice, or the post's on a post list; they are never the producer's. See Channels.

POST /v1/templates/sync, which appears in the route list, mirrors the template registry. It is Reelwire's own and refused to every key: nothing a producer or a publisher does needs it.

Mistakes people make​

Writing the payload by hand from an example. Take jsonSchema and generate a type. The example is there to check yourself against, not to copy.

Putting drawn content in metaData. It is never drawn. Headlines and figures go in clipData.

Sending locales beside the content. It goes inside the content, where the template validates it.

Pinning a version because it is there. A pin stops following new versions. Pin when you have a reason to, not by default.

Assuming every template comes in every shape. ratios is per build. A channel on a surface the template does not draw has nothing to render.

Addressing a feed by its slugs in stored configuration. Slugs are words and words get edited. Use the fd_ id.