Skip to main content

Channels

A channel is one account, in one format (a platform surface), speaking one language, wearing one brand. "Orchid Invest DE" on Instagram Reels is a channel; the same account posting to the Instagram feed is a different one, because the surface fixes the shape. What a channel makes is exactly the set of its subscriptions.

Reelwire posts nothing itself. The surface only shapes the clip, its caption and its hashtags; when a post is due it is handed over by email, by webhook or both, on the channel's two switches deliverByEmail and deliverByWebhook, and whoever receives it posts it. See How a post is handed over below.

MethodPathPermission
GET/v1/channelschannels:read
GET/v1/channels/surfaceschannels:read
POST/v1/channelschannels:write
PATCH/v1/channels/{id}channels:write
POST/v1/channels/{id}/deletechannels:write
POST/v1/channels/{id}/subscriptionschannels:write
PATCH/v1/channels/{id}/subscriptions/{subId}channels:write
POST/v1/channels/{id}/subscriptions/{subId}/deletechannels:write
PUT/v1/channels/{id}/credentialschannels:write
POST/v1/channels/{id}/credentials/deletechannels:write
GET/v1/connectionsconnections:read
PUT/v1/connections/modeconnections:write
GET/v1/deliveriesconnections:read
POST/v1/deliveries/{id}/redeliverconnections:write

A channel's own credentials are written under /v1/channels, so they need channels:write. How posts leave the workspace, and the log of what was delivered, are the Webhook - Delivery log area, connections: handing somebody the delivery log does not hand them every channel.

List channels​

curl http://localhost:4000/v1/channels \
-H "Authorization: Bearer rw_your_key_here"
{
"channels": [
{
"id": "01M1VFRWE7EWSY5D7QDQ0KNK1S",
"name": "Channel 1 en",
"platform": "instagram",
"surface": "instagram-reels",
"surfaceLabel": "Instagram Reels",
"ratio": "9x16",
"locale": "en",
"enabled": true,
"approval": "NONE",
"deliverByEmail": false,
"deliverByWebhook": true,
"posting": {
"allowComments": true,
"brandedContent": false,
"allowDuetStitch": false,
"hashtagsAsFirstComment": false,
"silent": false
},
"syndicates": false,
"syndicationToken": null,
"brand": { "id": "01M1VFRWE7VHP3H54ESRJQ0SAJ", "name": "Brand 1", "enabled": true },
"subscriptions": [
{
"id": "01M2985NZZ9NR9W21JJZ8VQQ4N",
"kind": "QUEUE",
"sourceSlug": null,
"formatSlug": null,
"streamId": null,
"queueId": "01M2985NYG2GJ450CQAF3DP5YK",
"queueName": "Enterprise 2 Post list 1",
"feedId": null,
"templateId": null,
"templateVariant": null,
"templateVersion": null,
"enabled": true
},
{
"id": "01M29DSF6S2JYYXQQM4XHJMGK7",
"kind": "BROADCAST",
"sourceSlug": "brk-test-feeds",
"formatSlug": "brk-feed-1",
"streamId": null,
"queueId": null,
"queueName": null,
"feedId": "fd_3e7b3bff3194",
"templateId": "tpl_a9c3fdb1b5c9",
"templateVariant": "Standard",
"templateVersion": null,
"enabled": true
}
]
}
]
}

posting only carries the switches the surface actually has, with its defaults filled in, so you never have to work out which apply.

The surface decides the shape​

GET /v1/channels/surfaces is the list to pick from, and it says which ratios each one offers.

{
"surfaces": [
{ "surface": "youtube-shorts", "platform": "youtube", "label": "YouTube Shorts", "ratios": ["9x16"] },
{ "surface": "youtube-video", "platform": "youtube", "label": "YouTube", "ratios": ["16x9"] },
{ "surface": "facebook-reels", "platform": "facebook", "label": "Facebook Reels", "ratios": ["9x16"] },
{ "surface": "facebook-feed", "platform": "facebook", "label": "Facebook Feed", "ratios": ["1x1"] },
{ "surface": "instagram-reels", "platform": "instagram", "label": "Instagram Reels", "ratios": ["9x16"] },
{ "surface": "instagram-feed", "platform": "instagram", "label": "Instagram Feed", "ratios": ["4x5", "1x1"] },
{ "surface": "tiktok", "platform": "tiktok", "label": "TikTok", "ratios": ["9x16"] },
{ "surface": "linkedin", "platform": "linkedin", "label": "LinkedIn", "ratios": ["1x1", "16x9"] },
{ "surface": "telegram", "platform": "telegram", "label": "Telegram", "ratios": ["9x16", "1x1", "16x9"] },
{ "surface": "download", "platform": "download", "label": "No specific platform", "ratios": ["9x16", "4x5", "1x1", "16x9"] }
]
}

"Facebook" is not an answer to the shape question: Reels want 9x16 and the page feed wants 1x1. A surface with one ratio fixes it, and ratio in the body is ignored. A surface with several asks, and leaving ratio out there is a 400 that names the choices.

No specific platform (download) is a surface of its own, and not a network: the format for a channel that does not name where its clips go. Every shape is offered because the person decides where it goes, and its caption is kept in three lengths, one for each kind of platform. The key is still download, the name it had when it was the one platform whose posts were emailed. Every new workspace starts with one, called "Default channel", handed over by email.

How a post is handed over​

FieldWhat it does
deliverByEmailWhen a post is due, the people who publish for the workspace (the owner and everybody holding Operations) get it by email, with a preview, a link to the clip and its text.
deliverByWebhookWhen a post is due, it is signed and sent to the workspace's webhook address. See Webhooks.
syndicates, syndicationTokenDeprecated and without effect, answered until the next major release. A channel no longer has a page of its own: posts are handed on to subscribers by group, see Syndication.

Both may be true: the post is then emailed and sent. Both false is refused with a 400 schema_validation naming the rule, on create and on update alike, since a post with neither would go nowhere. These two are the only ways out: the platform a channel names decides nothing about delivery.

A post going to several channels is handed over once per batch, not once per channel. Its channels that need no approval go together, in one email and one signed webhook call carrying every one of them, once all of them are rendered and due; each channel that asks for approval follows on its own once it is approved. See One call per hand-over and Posts and jobs.

Create a channel​

A name is enough. Everything else has a defensible default, and a channel that exists can be corrected in one call.

curl http://localhost:4000/v1/channels \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"name":"Orchid Invest DE","surface":"instagram-reels","locale":"de","brandId":"01M1VFRWE7VHP3H54ESRJQ0SAJ"}'
FieldRequiredDefault
namenoThe first free "New channel N". Up to 80 characters.
surfacenoinstagram-reels
ratiowhere the surface offers severalThe surface's only ratio. Required where it offers more than one.
localenoen. An ISO 639-1 code, or one of zh-CN, zh-TW, pt-BR.
brandIdnoThe workspace's oldest enabled brand.
approvalnoNONE. PER_POST holds every post for a reviewer.
deliverByEmailnotrue.
deliverByWebhooknofalse. At least one of the two must be true.
postingnoThe surface's own defaults.

201, with the channel as { "channel": { ... } }. A name already in use is a 409.

Two refusals worth knowing. Without a brand anywhere in the workspace: "Make a brand first: every channel wears one." And without a webhook: "This account has no webhook yet. Set one under Webhook first." One webhook serves the whole workspace, reused by every channel, so this is set once.

PATCH /v1/channels/{id} takes the same fields, and enabled to pause or resume it. Changing the surface, ratio, language or brand changes what the channel renders from now on, which is the reason it is allowed. Work already handed over is untouched: a post records the shape and language it went out in. A switch sent alone is laid over the other one as it stands, so turning Email off on a channel whose Webhook is off is the same 400 as sending both off.

POST /v1/channels/{id}/delete removes a channel and answers { "deleted": "...", "cancelled": 2 }. Nothing more is rendered or handed over for it, every job still waiting is cancelled (cancelled counts them), and its stored credentials are forgotten. What it already handed over stays where it was posted: removing a channel never asks anybody to take a post down. Its history stays too, under its name with " (removed)" on the end, so a new channel can take the old name.

Subscriptions: what a channel listens to​

A subscription names exactly one input, and kind says which.

kindNames the input withTemplate comes from
BROADCASTsourceSlug and formatSlugThe feed's binding in the catalogue
STREAMstreamIdThe stream
QUEUEqueueIdEach post on the list

The source fixes the template; the channel picks the cut and the build. A producer writing to a stream was promised a shape, and a channel does not get to overrule it. What the channel chooses is templateVariant, and templateVersion, which is an exact major.minor.patch or "latest".

curl http://localhost:4000/v1/channels/01M1VFRWE7EWSY5D7QDQ0KNK1S/subscriptions \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"kind":"STREAM","streamId":"st_d84d89e39bfc","templateVariant":"Standard","templateVersion":"latest"}'

201, and the whole channel comes back, with the new subscription among its list:

{
"channel": {
"id": "01M1VFRWE7EWSY5D7QDQ0KNK1S",
"name": "Channel 1 en",
"subscriptions": [
{
"id": "01M2SQZVBYJJ5YF2QFYNEG5A78",
"kind": "STREAM",
"sourceSlug": null,
"formatSlug": null,
"streamId": "st_d84d89e39bfc",
"queueId": null,
"queueName": null,
"feedId": null,
"templateId": "tpl_a9c3fdb1b5c9",
"templateVariant": "Standard",
"templateVersion": null,
"enabled": true
}
]
}
}

The channel's other fields are there too, exactly as GET /v1/channels shows them.

"latest" is stored as null, which is a real answer rather than an absence: it follows the newest build, resolved when something renders. Anything else must be an exact three-part version, because a pin that is almost a version is a pin to nothing, and a variant or version the template has no build for is refused rather than left to render nothing. A QUEUE subscription pins nothing: each post on the list chooses its own.

Subscribing a channel twice to the same input is a 409. A shipped feed the account is not offered is a 403 naming the feed.

PATCH /v1/channels/{id}/subscriptions/{subId} changes the variant, the version or enabled, and returns the channel. POST /v1/channels/{id}/subscriptions/{subId}/delete removes it and returns the channel as it now stands. The input itself, the feed, stream or list, is untouched.

The retired connector, and the delivery log​

Reelwire posts nothing itself, so the routes that set up posting are retired. They still answer, each with a Deprecation: true header, until the next major release, and then go:

  • GET /v1/connections answers the workspace's mode, which is always SELF_HOSTED now, and the credential fields channels once stored.
  • PUT /v1/connections/mode takes { "mode": "SELF_HOSTED" } and refuses MANAGED with a 409: Reelwire no longer posts with stored credentials.
  • PUT /v1/channels/{id}/credentials and POST /v1/channels/{id}/credentials/delete still store and forget a channel's platform credentials, and nothing reads them.
  • GET /v1/connector/env, /v1/connector/script/{language} and /v1/connector/adapters/{platform} answer the old self-hosted connector, written for the first version of the webhook. Do not build on it: build a receiver for Webhooks instead.

How a channel's posts leave the workspace is its own deliverByEmail and deliverByWebhook: see How a post is handed over above.

GET /v1/deliveries is the log of every way every hand-over went out, one line for its email and one for its webhook call, and GET /v1/deliveries/{id} one line whole, with what it carried and what came back. POST /v1/deliveries/{id}/redeliver sends one again. All three are on Webhooks, with examples.

Mistakes people make​

Treating platform as the destination. The surface is the destination. Instagram Reels and the Instagram feed are different channels with different shapes.

Choosing a ratio the surface does not offer. It is refused, or ignored where the surface has only one.

Expecting one channel to post in two languages. It cannot. A channel speaks one language, and two languages is two channels subscribed to the same input.

Setting templateId on a subscription. The input fixes it. Send templateVariant and templateVersion.

Pinning templateVersion and never revisiting it. A pin stops following new builds. Send "latest" unless you have a reason.

Deleting a channel to pause it. PATCH with enabled: false. A paused channel keeps its subscriptions and can be resumed; a removed one keeps only its history, and what was waiting for it is cancelled.