Streams and feeds
A stream is an address your own systems push data to. It carries one template and, where your document does not already fit that template, a field map that says where each of the template's inputs lives in your document. Channels subscribe to the stream and inherit both, so there is exactly one place the mapping is written and exactly one place it is checked.
Feeds are the other half: the data Reelwire ships, which a workspace switches on rather than creates. These routes configure both. Sending to a stream is Sending events.
| Method | Path | Permission |
|---|---|---|
GET | /v1/streams | streams:read |
POST | /v1/streams | streams:write |
POST | /v1/streams/{id}/update | streams:write |
POST | /v1/streams/{id}/delete | streams:write |
POST | /v1/streams/{id}/suggest-map | streams:write |
GET | /v1/feeds | feeds:read |
PUT | /v1/feeds | feeds:write |
GET | /v1/feeds/{feedId}/reference | feeds:read |
GET | /v1/feeds/{feedId}/reference/{ratio} | feeds:read |
Streams and feeds are two permission areas, Custom feeds (streams) and Feeds (feeds),
because they are two different things: a stream is your own pipe, a feed is something Reelwire
carries. /v1/streams and /v1/stream are different routes in different areas too. The plural one
configures streams and needs streams; the singular one takes events and needs ingest:write.
Create a stream
curl http://localhost:4000/v1/streams \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Docs examples stream",
"description": "Created while writing the reference",
"templateId": "tpl_a9c3fdb1b5c9",
"fieldMap": {
"/clipData/headline": { "from": "/article/title", "as": "string", "maxLength": 80 },
"/clipData/source": { "const": "Orchid Invest" }
}
}'
{
"stream": {
"id": "st_d84d89e39bfc",
"name": "Docs examples stream",
"description": "Created while writing the reference",
"enabled": true,
"templateId": "tpl_a9c3fdb1b5c9",
"fieldMap": {
"/clipData/source": { "const": "Orchid Invest" },
"/clipData/headline": { "as": "string", "from": "/article/title", "maxLength": 80 }
},
"subscribers": 0,
"listeners": []
}
}
201. The body:
| Field | Required | What it is |
|---|---|---|
id | no | st_ and twelve hex characters, where you want to choose it. Omit it and Reelwire draws one. An id already taken in any workspace is a 409. |
name | yes | 1 to 120 characters. |
description | no | Up to 500 characters. |
enabled | no | Defaults to true. |
templateId | yes | The template every event on this stream is drawn with, one this workspace is offered. Any other is a 404. |
fieldMap | no | Defaults to {}, which is pass-through. |
A templateVariant or templateVersion sent here is dropped: which cut and which build a clip is
drawn with is the choice of each channel that subscribes.
The field map
A map is flat: each key is a JSON Pointer into the template's input document, and each value says where that input comes from in your document.
| Key | What it does |
|---|---|
from | A JSON Pointer into the posted data. |
const | A fixed value, for an input your data has no equivalent for. |
fallback | Used when from resolves to nothing. Absent is not the same as empty. |
as | Coercion before validation: string, number, integer, boolean or iso-date. |
maxLength | Cut over-long text to the template's limit rather than failing validation. |
Exactly one of from and const is required per entry. Keys must start with /.
An empty map means pass-through, not "drop everything", and it is now the ordinary case: a template
publishes the shape it accepts at /v1/formats/template/{templateId}, so a producer written against
that already emits the right document and has nothing to map.
You can see a map run without sending anything, on /v1/stream/validate. The raw document goes in
and document comes back as the template will see it:
curl http://localhost:4000/v1/stream/validate \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"streamId": "st_d84d89e39bfc",
"eventId": "01M2SQMKK3G1NVFXV31DCBVB95",
"occurredAt": "2026-09-18T08:00:00Z",
"data": {
"article": { "title": "Orchid Invest opens a Frankfurt desk", "body": "..." },
"publishedBy": "newsroom"
}
}'
{
"requestId": "01M2SQYACSHKW9FHFD8RYAK1WT",
"streamId": "st_d84d89e39bfc",
"templateId": "tpl_a9c3fdb1b5c9",
"templateVersion": "1.0.0",
"valid": true,
"document": {
"clipData": {
"source": "Orchid Invest",
"headline": "Orchid Invest opens a Frankfurt desk"
}
},
"warnings": [],
"errors": [],
"wouldMatch": []
}
wouldMatch is empty because no channel subscribes to this stream yet. The document is valid all
the same.
Let Reelwire suggest a map
POST /v1/streams/{id}/suggest-map matches your own field names against the template's inputs and
proposes a map. It needs one real payload to work from, and says so plainly when there is none:
curl http://localhost:4000/v1/streams/st_d84d89e39bfc/suggest-map \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"type": "https://reelwire.io/errors/no_sample",
"code": "no_sample",
"message": "This stream has not received anything yet",
"requestId": "01M2SQYKXH7B2JW3PWQ24YHV3Q",
"details": [
{
"path": "",
"message": "Send one event from your script first. The suggestion is built from your own field names, so there is nothing to match against until then."
}
]
}
}
Send one event, then ask again. A stream keeps the most recent payload it received for exactly this.
Once it has one, the answer is suggestion (a map to review, never applied by itself),
templateFields (every input the template has) and sample (the payload it matched against). Send
templateId, and templateVersion if you like, to see a map for another template before switching.
List streams
curl http://localhost:4000/v1/streams \
-H "Authorization: Bearer rw_your_key_here"
{
"route": "http://localhost:4000/v1/stream",
"streams": [
{
"id": "st_30472168d941",
"name": "Enterprise 2 Stream 1",
"enabled": true,
"templateId": "tpl_a9c3fdb1b5c9",
"fieldMap": {},
"lastReceivedAt": "2026-09-18T07:45:50.088Z",
"subscribers": 2,
"listeners": [
{ "channelId": "01M2985NX4V0WA1GV9MC63DEPJ", "channelName": "Channel 4 de" },
{ "channelId": "01M2985NXJRXVE5HKCCGKASP8P", "channelName": "Channel 10 en" }
]
}
]
}
subscribers is how many channels consume the stream. Zero means nothing renders, whatever you
send. Each stream also carries lastSample, the most recent payload it received, once it has one.
Change or remove a stream
POST /v1/streams/{id}/update takes the same fields as create, all of them optional, except id:
the id is deliberately not editable, because your script names it in every request and changing it
would look from your side like Reelwire returning 404 for no reason. Changing templateId or
fieldMap is checked against the template's newest build before it is saved, and against the
stream's last payload where it has one, so a map that saves is a map that renders. It answers
{ "ok": true }. A new template sends every subscribing channel back to the template's first
variant and its newest build, because what they had chosen belonged to the old template.
POST /v1/streams/{id}/delete removes the stream and answers { "ok": true, "stopped": 2 }: from
then on events sent to it are refused, and the channels that listened stop, which is what stopped
counts. What it had already received still renders and posts. The stream's history stays, under its
name with " (removed)" on the end, so check listeners before you call it.
Feeds
GET /v1/feeds lists every shipped feed the account is offered, grouped by source, with the
template each is drawn through, the channels listening to it and when it is expected to post. A feed
the account is no longer offered still appears while a channel listens to it.
curl http://localhost:4000/v1/feeds \
-H "Authorization: Bearer rw_your_key_here"
{
"sources": [
{
"slug": "brk-test-feeds",
"milestone": 1,
"entitled": false,
"live": true,
"formats": [
{
"id": "fd_3e7b3bff3194",
"slug": "brk-feed-1",
"name": "Fin Feed 1",
"title": "Fin Feed 1",
"description": "Feed description 1",
"templateId": "tpl_a9c3fdb1b5c9",
"templateName": "Template 1",
"templateVariant": "Standard",
"maxAgeSeconds": null,
"live": true,
"offered": true,
"enabled": true,
"subscribers": 5,
"schedule": { "every": "daily", "time": "07:00" },
"scheduleLabel": "Daily 07:00"
}
]
}
]
}
id is the feed id, fd_ and twelve hex characters. Address a feed by its id wherever one is
asked for: the slugs are words and words get edited, and a producer pinned to a title breaks the day
somebody fixes its capitalisation.
GET /v1/feeds/{feedId}/reference lists the example clips a feed has, by variant and version, with
the shapes each comes in. .../reference/{ratio} returns one of them, an MP4 rather than JSON;
narrow it with ?variant= and ?version=, and the version it served is in x-preview-version.
PUT /v1/feeds switches one feed on or off for the whole workspace. It takes sourceSlug,
formatSlug and enabled, and enabled must be a boolean rather than a string. A feed the
workspace is not offered, and no channel listens to, is a 403.
curl -X PUT http://localhost:4000/v1/feeds \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"sourceSlug":"brk-test-feeds","formatSlug":"brk-feed-1","enabled":true}'
{ "sourceSlug": "brk-test-feeds", "formatSlug": "brk-feed-1", "enabled": true }
Switching a feed off here stops it for every channel at once. Removing one channel's subscription is a channel operation; see Channels.
Mistakes people make
Confusing /v1/stream with /v1/streams. One takes events, the other configures them, and
they need different permissions. The prefix matching is exact, so a key with ingest:write alone
gets a 403 on the plural route, and a key with streams:write alone cannot send.
Writing a field map when the producer could just emit the right shape. Ask
/v1/formats/template/{templateId} what the template accepts. A map exists for data you do not
control, not as the normal way in.
Nesting the map to mirror the target. It is flat. "/instrument/symbol" is one key, not two
levels of object.
Pinning a template version on the stream. You cannot: the stream carries the template, and which cut and which build are the subscribing channel's choice. See Channels.
Deleting a stream to "reset" it. Its subscriptions stop with it, every channel that listened
stops receiving, and the id cannot be used again. Pause it with enabled: false instead, which
answers events with a 409 stream_disabled until you switch it back on.