Skip to main content

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.

MethodPathPermission
GET/v1/streamssources:read
POST/v1/streamssources:write
POST/v1/streams/{id}/updatesources:write
POST/v1/streams/{id}/deletesources:write
POST/v1/streams/{id}/suggest-mapsources:write
GET/v1/feedssources:read
PUT/v1/feedssources:write
GET/v1/feeds/{feedId}/referencesources:read
GET/v1/feeds/{feedId}/reference/{ratio}sources:read

/v1/streams and /v1/stream are different routes in different permission areas. The plural one configures streams and needs sources; 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": []
}
}

The body:

FieldRequiredWhat it is
idnost_ and twelve hex characters, where you want to choose it. Omit it and Reelwire draws one.
nameyes1 to 120 characters.
descriptionnoUp to 500 characters.
enablednoDefaults to true.
templateIdyesThe template every event on this stream is drawn with.
fieldMapnoDefaults to {}, which is pass-through.

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.

KeyWhat it does
fromA JSON Pointer into the posted data.
constA fixed value, for an input your data has no equivalent for.
fallbackUsed when from resolves to nothing. Absent is not the same as empty.
asCoercion before validation: string, number, integer, boolean or iso-date.
maxLengthCut 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.

List streams

curl http://localhost:4000/v1/streams \
-H "Authorization: Bearer rw_your_key_here"
{
"streams": [
{
"id": "st_30472168d941",
"name": "Enterprise 2 Stream 1",
"enabled": true,
"templateId": "tpl_a9c3fdb1b5c9",
"fieldMap": {},
"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.

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.

POST /v1/streams/{id}/delete removes the stream. Channels that subscribed to it lose that subscription, 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 and the channels listening 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": "Brk Feed 1",
"title": "Brk 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 and .../reference/{ratio} return the reference clip for a feed, which is what the feed looks like drawn.

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.

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.

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 go with it, and every channel that listened stops receiving. Disable it with enabled: false instead.