Skip to main content

Post lists

A post list is the third way data arrives: somebody writes it, rather than a feed pushing it. The list is a name; the posts on it each choose their own template, hold the content typed into that template's inputs, and say when they go out. At that moment a post becomes an event like any other and takes the same path, on every channel listening to the list.

The API calls these queues, and a post on one is a package. On the dashboard they are on Post composer, in its Manual post lists card, where the words are post list and post; the permission area is Post composer (postlists).

MethodPathPermission
GET/v1/queuespostlists:read
POST/v1/queuespostlists:write
PATCH/v1/queues/{id}postlists:write
POST/v1/queues/{id}/deletepostlists:write
POST/v1/queues/{id}/packagespostlists:write
PATCH/v1/queues/{id}/packages/{packageId}postlists:write
POST/v1/queues/{id}/packages/{packageId}/deletepostlists:write

Create a list​

A list is a name, and nothing else. Its posts are added one at a time.

curl http://localhost:4000/v1/queues \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"name":"Docs examples list"}'
{
"queue": {
"id": "01M2SQRG1HVVSB4P1D04WT0T8Z",
"name": "Docs examples list",
"enabled": true,
"posts": [],
"subscribers": 0,
"listeners": [],
"brands": []
}
}

201. name is 1 to 80 characters and must be free within the workspace; a repeat is a 409. PATCH /v1/queues/{id} takes name and enabled, and answers with the list as queue.

A list switched off (enabled: false) holds nothing back: a post that comes due while it is off still fires, reaches no channel and is not sent again later. Switch it off to stop a list, not to pause it; to hold one post, change its time.

A workspace holds up to 50 lists and 5,000 posts across all of them unless Reelwire has set it otherwise; past that, a 409 names the number. Posts that already went out count until they are deleted.

Schedule a post​

curl http://localhost:4000/v1/queues/01M2SQRG1HVVSB4P1D04WT0T8Z/packages \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Weekly market wrap",
"templateId": "tpl_39bc5fecfa42",
"templateVariant": "Corporate glass",
"scheduledFor": "2026-09-25T08:00:00Z",
"data": {
"clipData": {
"title": "Weekly market wrap",
"items": [
{ "text": "Bund yields up", "detail": "The ten-year closed at 2.41 per cent" },
{ "text": "Euro steady" },
{ "text": "Oil lower" }
],
"source": "Orchid Invest"
},
"metaData": {
"title": "Weekly market wrap",
"captionShort": "Three things that moved the week.",
"hashtags": ["markets"]
}
}
}'
{
"package": {
"id": "01M2SQRR3PB0MQ2M2GP9A0ZT82",
"title": "Weekly market wrap",
"templateId": "tpl_39bc5fecfa42",
"templateVariant": "Corporate glass",
"templateVersion": null,
"data": {
"clipData": {
"items": [
{ "text": "Bund yields up", "detail": "The ten-year closed at 2.41 per cent" },
{ "text": "Euro steady" },
{ "text": "Oil lower" }
],
"title": "Weekly market wrap",
"source": "Orchid Invest"
},
"metaData": {
"title": "Weekly market wrap",
"hashtags": ["markets"],
"captionShort": "Three things that moved the week."
}
},
"locales": null,
"scheduledFor": "2026-09-25T08:00:00.000Z",
"state": "SCHEDULED",
"firedAt": null,
"ingestEventId": null,
"error": null,
"waitingForPicturesSince": null,
"waitingFor": [],
"outcome": null
}
}

The body:

FieldRequiredWhat it is
titleyes1 to 120 characters. The post's name on the list, not the post's own title, which is metaData.title.
templateIdyesThe template this post is drawn with, one this workspace is offered. Any other is a 404.
templateVariantnoWhich cut, by its name, such as Corporate glass. Absent is the template's first. A cut the template does not have is a 400 that names the ones it has.
templateVersionnoAn exact major.minor.patch, or "latest". Absent and "latest" both store null, which follows the newest build when the post fires. A version that does not exist is a 400. For now every template is at 1.0.0.
scheduledForyesAn ISO instant, or the string "now".
datayesThe content, in the template's own input shape.

201. There is no field map here. A post is written straight into the template's inputs, so there is nothing to map from: ask GET /v1/formats/template/{templateId} what they are, with ?variant= for a template whose variants each take their own content. The example is List, whose clip is a title and up to twelve items, each a text with an optional detail. A picture in data is a file from the media library, { "url": "media://...", "alt": "..." }; a web address is refused. See Sending events for what goes in clipData and metaData.

Language copies travel inside data, under a locales block that the template checks, not in a locales field of the request. The response always shows locales: null; that column is no longer written from a request.

Content is checked when you save, not when it fires​

The same validator that guards a stream checks what you send, against the build the post would actually draw with, at the moment you save it. A post that saves is a post that renders.

curl http://localhost:4000/v1/queues/01M2SQRG1HVVSB4P1D04WT0T8Z/packages \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Missing its points",
"templateId": "tpl_39bc5fecfa42",
"scheduledFor": "now",
"data": { "clipData": { "title": "Missing its points" } }
}'
{
"error": {
"type": "https://reelwire.io/errors/schema_validation",
"code": "schema_validation",
"message": "The post does not fit the template",
"requestId": "01M2SQS36D5F7W5S7WSZPHMADF",
"details": [{ "path": "/clipData", "message": "missing items" }]
}
}

The failure lands with the person who can fix it, rather than in a worker at six on a Sunday. Each detail names a field and what is wrong with it: missing, not one this template takes, over its limit, or not one of the values it accepts. It is checked against the post's own variant, since the variants of a vertical template (Finance, Influencer, Retail, Tech, Public bodies) each take their own content. The check looks at the shape of a picture, not at the library: a media:// reference to a file the library does not hold saves, and draws as a missing picture. POST /v1/templates/{templateId}/validate names those too, before you save.

A time in the past is refused​

A time more than a minute gone is a mistake, not an instruction:

{
"error": {
"type": "https://reelwire.io/errors/schema_validation",
"code": "schema_validation",
"message": "That time has already passed. Pick Right away, or a time to come.",
"requestId": "01M2SQS34CCN1K6S6MZXKD631Y",
"details": []
}
}

Send "scheduledFor": "now" where you mean immediately.

Read a list back​

curl http://localhost:4000/v1/queues \
-H "Authorization: Bearer rw_your_key_here"
{
"queues": [
{
"id": "01M2985NYG2GJ450CQAF3DP5YK",
"name": "Enterprise 2 Post list 1",
"enabled": true,
"posts": [],
"subscribers": 3,
"listeners": [
{ "channelId": "01M1VFRWE7EWSY5D7QDQ0KNK1S", "channelName": "Channel 1 en", "locale": "en", "brandId": "01M1VFRWDZ6KBJ1T4Q3V5N8X2C", "brandName": "Brand 1" },
{ "channelId": "01M2985NXBN97TVSF6AN19TDN2", "channelName": "Channel 7 en", "locale": "en", "brandId": "01M1VFRWDZ6KBJ1T4Q3V5N8X2C", "brandName": "Brand 1" },
{ "channelId": "01M2985NXSH4KRQHCP6WJ2KMFR", "channelName": "Channel 13 en", "locale": "en", "brandId": "01M1VFRWDZ6KBJ1T4Q3V5N8X2C", "brandName": "Brand 1" }
],
"brands": [
{
"id": "01M1VFRWDZ6KBJ1T4Q3V5N8X2C",
"name": "Brand 1",
"text": {
"description": "Trading involves risk.",
"link": { "label": "Read more", "url": "https://example.com/more" },
"hashtags": ["markets"]
},
"locales": { "de": { "description": "Handel birgt Risiken.", "hashtags": ["maerkte"] } }
}
]
}
]
}

Lists come in name order, and the posts on each (posts, in the shape above) by the time they go out. listeners is every channel listening to the list, with its language (locale) and its brand: those languages are the ones worth writing a post's locales for, since each channel draws its own language's copy laid over the default. brands is each brand of the listening channels once, with what it adds after every post's own text (text: its description, link and hashtags), and the same in each language the brand wrote its own post text for (locales, by language code).

A post's state is SCHEDULED until its time, then FIRED; CANCELLED once cancelled, and FAILED, with the reason in error, when it could not fire at all, such as a template left with no build to render with.

A post whose content names a picture (media://) the media library does not hold is never fired without it: when its time comes it stays SCHEDULED, waitingForPicturesSince says since when, waitingFor lists the file names, and it fires as soon as every one is in the library, checked about once a minute. Both are null and [] for a post not waiting. A post may be written naming a picture still to come, so a plan can be made before its photos are taken; whoever publishes is emailed a few days before it is due while a picture is missing. Changing its data or scheduledFor starts it over.

Each post carries an outcome once it has fired, counted over the channels it reached: channels, rendering, rendered, renderFailed, published, handedOver (handed over by email or webhook, and nobody has reported it published yet), emailed (the same number, under the name it had while email was the only way), awaitingApproval and publishFailed.

outcome is null for a post still to go. A post fires whether or not anybody listens, so "sent" alone cannot say whether a video was made: this can.

Changing and cancelling​

PATCH /v1/queues/{id}/packages/{packageId} accepts title, scheduledFor, templateId, templateVariant, templateVersion, data, and state where state is "CANCELLED" (any other state is a 400). It answers with the post as package. A new template, cut, build or content is checked again as a whole, because content that fitted the old template may not fit the new one.

Two things to know. Only a post still in SCHEDULED can be changed; a post that went out is a record of something that happened, and editing it would rewrite history. And changing the time re-validates the content, because a template that prints when the fact happened reads it from the content, where the time a post fires is written in for it (occurredAt, only for a template that takes one).

POST /v1/queues/{id}/packages/{packageId}/delete removes a post from the list and answers { "ok": true }.

POST /v1/queues/{id}/delete removes the list and every post on it, and unsubscribes the channels that were listening, together with the posting jobs they made from it and their clips. The response says how many channels:

{ "ok": true, "removedFromChannels": 3 }

Getting a list published​

Making a post list does not publish anything on its own. A channel has to subscribe to it, with kind: "QUEUE"; see Channels. A post fires whether or not anybody listens, and one nobody listened to made nothing: its outcome says channels: 0. The channel pins nothing for a list: each post chooses its own template, cut and build. Whether a post then waits for somebody to approve it is the channel's own approval setting, as it is for every other source.

What each post says beside its clip is what was written in its metaData: the title, and the description in the length that fits each listening channel's platform (the four lengths and which platforms take which are on Templates and formats). Write the lengths your channels need; one left out is filled from the next shorter one written, or failing that the next longer one. Nothing is written for a manual post behind the writer's back: a description or hashtags left empty go out empty. Only an empty title is filled in, from the clip's own title (its clipData title and label, or headline), or the list's name where there is none, because some platforms will not take a video without one.

Making a post again​

GET /v1/jobs/{id}/reuse answers with what a finished post was made from. Send its templateId, variant as templateVariant, pinnedVersion as templateVersion and data to POST /v1/queues/{id}/packages, with a title and a scheduledFor, and the post goes out again with whatever you changed; for a post that came from a list, queueId names the list. On the dashboard, Reuse on the Posts screen, As a new post, opens Add new post on Post composer with all of it filled in.

Plan the next weeks​

GET /v1/queues/{id}/plan answers when the next weeks of posts on this list can go out, and how many fit: what an assistant's content plan is built on (reelwire_plan_posts). It reads and changes nothing, and it never says what a post should say.

QueryWhat it is
weeksHow many weeks ahead, 1 to 13. Default 12.
fromWhen the plan starts, ISO 8601. Default now.
timeZoneWhere the audience lives, an IANA zone such as Europe/Berlin. Default a guess from the first listening channel's language, answered with timeZoneAssumed: true.
postsPerWeek1 to 21. Default what the allowance pays for, at most three.
daysWeekdays, Mon,Wed,Fri or 1,3,5, instead of spreading postsPerWeek evenly.
timesLocal times, HH:MM between 06:00 and 20:00, at most three. Default 09:00.

Every slot keeps three rules. The allowance, period by period: a post renders once for every channel listening to the list, and what a period has left is its videos less those used, less what the channels already have coming (the Posting calendar's entries, feeds and lists alike), less what the plan adds. The ramp, per channel and local day: at most one post a day in a channel's first week, two in weeks two and three, three from week four, counted from its first post. Daylight: every time is between 06:00 and 20:00 where the audience lives. A slot that breaks one is in dropped, with why (allowance or ramp).

{
"queue": { "id": "01M2SQRG1HVVSB4P1D04WT0T8Z", "name": "Default post list", "enabled": true },
"window": { "from": "2026-10-26T00:00:00.000Z", "to": "2027-01-18T00:00:00.000Z", "weeks": 12 },
"timeZone": "Europe/Berlin",
"timeZoneAssumed": false,
"rendersPerPost": 1,
"postsPerWeek": 3,
"slots": [
{ "at": "2026-10-26T08:00:00.000Z", "local": "Mon 2026-10-26 09:00", "week": 1 },
{ "at": "2026-10-28T08:00:00.000Z", "local": "Wed 2026-10-28 09:00", "week": 1 }
],
"dropped": [],
"droppedCount": { "allowance": 0, "ramp": 0 },
"periods": [
{ "start": "2026-10-12T09:30:00.000Z", "end": "2026-11-12T09:30:00.000Z", "allowance": 15, "used": 2, "booked": 0, "planned": 5, "left": 8, "morePosts": 8 }
],
"channels": [
{ "id": "01M1W0W9Q1ZB5Y8WQF8R8C3V7K", "name": "Default channel", "locale": "de", "firstPostAt": null, "weekAtStart": 1, "postsPerDayAtStart": 1 }
]
}

Write a whole plan at once​

POST /v1/queues/{id}/packages/batch takes posts, a list of up to 100, each with the fields of a single post (title, templateId, templateVariant, templateVersion, scheduledFor, data). Every post is checked exactly as one is, and nothing is written unless all of them fit: a refusal is a 400 whose details name each post that does not by its number, /posts/3/clipData/title. The answer is packages, the posts written, in time order, with a 201.

Mistakes people make​

Waiting for a validation failure at fire time. There is not one. The content is checked when you save it, against the build it would draw with.

Scheduling into the past to publish now. It is refused. Use "now".

Sending locales beside data. Language copies go inside data, where the template validates them.

Editing a post that already fired. A 409. Create a new post.

Pinning templateVersion and forgetting it. A pin to an exact build stops following new ones. Send "latest", or leave it out, unless you have a reason.

Deleting a list to tidy up. Every channel listening to it loses that subscription, and the posting jobs it made go with it. Check listeners first, or set enabled: false, which keeps the subscriptions.

Switching a list off to pause it. A post that comes due while the list is off fires to nobody and is not sent later. Move the post's time, or cancel it, instead.