Skip to main content

Syndication

Syndication hands a workspace's channels' posts on to its participants: whoever subscribes in one of its groups' iframes. The workspace makes groups of its channels, each with an iframe of its own that it puts in its own portal. In it a participant chooses the group's channels they want and subscribes an email address and/or a webhook, as the group allows, each confirmed by email, and receives every post of those channels when it is handed over to the workspace. Nothing comes back from a participant: they hold no key and report nothing. The API calls participants partners (/v1/syndication/partners), as the plan's allowance does.

Syndication is part of Enterprise. On any other plan every route below that changes, sends or draws something answers 409, previews and test emails included; reading still works, and so do removing a group and removing a participant, so a workspace that moved down a tier can still clear what it had. How many participants a workspace may have is its syndication participants allowance, partners in GET /v1/syndication/settings, counting the ones who can receive something.

MethodPathPermission
GET/v1/syndication/settingssyndication:read
PUT/v1/syndication/settingssyndication:write
POST/v1/syndication/previewsyndication:read
POST/v1/syndication/sender/testsyndication:write
GET/v1/syndication/groupssyndication:read
GET/v1/syndication/groups/{id}syndication:read
POST/v1/syndication/groupssyndication:write
PATCH/v1/syndication/groups/{id}syndication:write
POST/v1/syndication/groups/{id}/deletesyndication:write
POST/v1/syndication/groups/{id}/partnerssyndication:write
GET/v1/syndication/partnerssyndication:read
GET/v1/syndication/partners/{id}syndication:read
PATCH/v1/syndication/partners/{id}syndication:write
POST/v1/syndication/partners/{id}/deletesyndication:write
GET/v1/syndication/deliveriessyndication:read
POST/v1/syndication/deliveries/{id}/redeliversyndication:write

The preview is a POST only because what it draws is sent as a document; it changes nothing, so a key that may only read syndication may ask for one.

Groups​

curl -X POST http://localhost:4000/v1/syndication/groups \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "All channels",
"description": "Our daily clips, ready to post: choose the channels you want.",
"channelIds": ["01M1VFRWE7EWSY5D7QDQ0KNK1S", "01M1VFRWE7EWSY5D7QDQ0KNK1T"],
"byEmail": true,
"byWebhook": true,
"allowedOrigins": ["https://portal.example.com"]
}'
FieldWhat it is
nameThe workspace's own name for it, up to 80 characters. Never shown to participants.
pageTitleOptional: the heading of its iframe, up to 100 characters; null or left out reads "Ready-to-post videos from" and the branding's title.
descriptionOptional: what participants read at the top of its iframe, up to 300 characters.
channelIdsIts channels, each one of yours and not removed, at most 500. The iframe offers them to choose from; with one there is nothing to choose, and every participant receives it. An id that is not one of your channels is a 404 naming it.
byEmail, byWebhookHow its posts reach participants: an email with the post kit, a call to their webhook, or both. At least one; both by default. The group decides, whatever its channels hand over by.
allowedOriginsThe sites whose pages may show it, as origins such as https://portal.example.com (a path is dropped), at most 20. Sent as the page's Content-Security-Policy: frame-ancestors, and a page naming sites does not open on its own. Empty or left out is any site.
showHeaderWhether the page shows the logo and title above its cards. true by default; false for a portal that frames it under a header of its own.
positionPATCH only: where the group stands in the list, from 0.

Every group comes back with page.url, its page's address, page.frame, the iframe code to copy into the portal's HTML (it carries allow="clipboard-write", so the page's Copy buttons work inside the frame), portalKey, the key to sign participant IDs and email addresses with (below), channels, each of its channels with its name, surface, ratio and language, position, and partners, how many subscribed in it. GET /v1/syndication/groups answers them all in their order, and GET /v1/syndication/groups/{id} one of them. PATCH changes any of the fields above; a channel added is offered to its participants as a new choice, one taken out is no longer handed on. POST .../delete removes it with its page and its participants. A page's address never changes: a group whose iframe went astray is replaced by a new group. While the workspace is not on Enterprise, every group's page answers 404.

What reaches participants never depends on a channel's own ways out (deliverByEmail, deliverByWebhook, deliverByConnector): a channel with all of them off still hands its posts on to every participant whose group takes it. A participant's webhook is given five seconds to answer, and one that does not is tried again on the retry ladder, so a slow receiver never holds up the others. GET /v1/syndication/deliveries is the log of every email and call, and takes groupId beside its other filters; each line names its group.

One number bounds both groups and participants: the plan's syndication participants allowance. A workspace may hold that many groups, and that many participants across all of its groups together (only those who can receive something count), so one group may hold them all, or every group one. One group more is refused with 409 and a sentence naming the number.

Each group also has one webhook signing secret, made with it. It is shown in the group's iframe, to participants, beside the webhook field. The one route that answers it is adding a participant with a webhook (below), once, so the workspace can pass it on; no other route returns it.

The page tells the frame around it how tall it is with a postMessage of { type: "partner-frame:height", height }.

Participant IDs and email addresses​

A portal that knows who is signed in can name them to the page, which then shows them what they subscribed with. Add two values to the iframe's address for the portal's own ID for them, two for their email address, or all four:

<iframe src="https://reelwire.io/embed/<code>?participant=C-1001&sig_participant=<signature>&email=anna%40smith.test&sig_email=<signature>" ...></iframe>

participant is the portal's own ID for them, their participant ID: 1 to 100 printable characters, no space at either end, URL-encoded in the address. sig_participant is the HMAC-SHA256 of the ID, keyed with the group's portalKey, in hex. email is their address and sig_email its signature, made the same way, of the address as passed or in lower case. With the address the page asks for none: confirmations go to it, and it finds their subscription by it. Make each signature on the portal's server:

import crypto from "node:crypto";
const key = process.env.REELWIRE_PORTAL_KEY;
const sig_participant = crypto.createHmac("sha256", key).update(participantId).digest("hex");
const sig_email = crypto.createHmac("sha256", key).update(email).digest("hex");
$key = getenv("REELWIRE_PORTAL_KEY");
$sig_participant = hash_hmac("sha256", $participantId, $key);
$sig_email = hash_hmac("sha256", $email, $key);

The key never goes to a browser. A page whose address names a participant or an address without the right signature answers 403 and shows nothing to subscribe with, and so does any of the page's calls carrying one: the ID and the address decide whose subscription the page shows, so an unsigned one would let anybody read and change another's by editing the address. A participant has one subscription per group, and customerId shows the ID on the participant. The first names of these values, customer, sig and esig, are still taken where the new ones are absent.

Subscribing and unsubscribing​

In the iframe a participant chooses the channels they want, where there are several, and subscribes an email address, a webhook or both, as the group allows, each in its own card. Every subscription, change of channels and unsubscription is confirmed by email, in two steps: the email's link opens a confirmation page, and only the button on that page acts (GET asks, POST does it), so a mail scanner opening links changes nothing:

  • An email address gets an email asking to confirm the subscription (/v1/partner/confirm/...). Nothing else is sent to it until the page's button is pressed. The same address cannot subscribe twice to one group.
  • A webhook is given with an email address to confirm it from, which receives no posts; it is not asked where the portal passed an address or the participant gave one before, and it is kept as contactEmail. The confirmation (/v1/partner/webhook/...) makes it the participant's webhook and calls it with a test, signed with the group's secret, which the iframe shows with example code for Node.js, Python, PHP and C#. No post goes to it until a test call has answered with a 2xx. The iframe always offers a test, before subscribing as well as after: it tries the address shown or typed, and only an answer from the participant's own webhook counts. A test of an address not theirs keeps nothing, and its ping carries no syndication note.
  • Join / Update, for somebody the portal did not name: an address or webhook that receives already gets an email asking to confirm the channels chosen now (/v1/partner/channels/...), and they change as it is confirmed. The channels a confirmation is for travel in its link, so somebody who receives something keeps their channels until then. A participant the portal named changes them at once.
  • Leave emails the confirmation: for the emails, to the address; for a webhook, to the address it was confirmed from; in use or still waiting alike (/v1/partner/stop/..., /v1/partner/webhook-stop/...). The stop link in every post email is the same page, and the one-click List-Unsubscribe-Post a mail app sends acts at once. A participant left with no way at all is deleted, with their log lines, rather than kept as unconfirmed.

A confirmation link works for three days; the stop link in a post email never expires, since a way out that expires is no way out. For somebody the portal did not name, every answer is the same whether the address or webhook is subscribed or not (neutral), so the page never says who is. There is no account and no settings page: a webhook also stops by answering a post's call with 410 Gone, and either way can subscribe again.

Each button in the iframe is a call the page makes to its own address, POST /embed/<code>/ followed by subscribe-email, subscribe-webhook, unsubscribe-email, unsubscribe-webhook or channels: they belong to the page, not to the API, and take ten calls an hour from one internet address. Send a test call is POST /embed/<code>/test, twenty a minute. At most five confirmation emails a day go to one participant, never two within a minute. A page that names its visitor asks POST /embed/<code>/state every five seconds whether anything changed, so a confirmation pressed in an email shows there without a reload. Participants who never confirm anything are removed after a week.

Participants​

GET /v1/syndication/partners answers a page of the participants, newest first, with total, page, perPage and room (used, the participants who can receive something, and max, the most allowed). It takes q (a participant ID, an address or a webhook), groupId, page, perPage (50 when left out, up to 500) and ascending. The dashboard shows the same list per group, behind Participants on the group's row. GET /v1/syndication/partners/{id} answers one, drawn as every entry of the list is:

{
"partner": {
"id": "01M3QZ2F6T0Y4G3X9J8K7N5B2C",
"label": "C-1001",
"customerId": "C-1001",
"source": "self",
"blocked": false,
"blockedAt": null,
"groupId": "01M3M6X0CB978K4MC4PC59706X",
"group": { "id": "01M3M6X0CB978K4MC4PC59706X", "name": "All channels" },
"channelIds": ["01M1VFRWE7EWSY5D7QDQ0KNK1S"],
"feeds": [{ "id": "01M1VFRWE7EWSY5D7QDQ0KNK1S", "name": "Daily news" }],
"paused": false,
"email": "anna@smith-invest.com",
"emailConfirmedAt": "2026-09-28T10:04:40.000Z",
"pendingEmail": null,
"pendingEmailAt": null,
"webhook": { "url": "https://smith-invest.com/hooks/posts", "since": "2026-09-28T10:02:11.000Z", "verifiedAt": "2026-09-28T10:03:02.000Z" },
"pendingWebhook": null,
"contactEmail": "anna@smith-invest.com",
"reachable": true,
"lastDelivery": null,
"createdAt": "2026-09-28T10:02:11.000Z",
"updatedAt": "2026-09-28T10:04:40.000Z"
}
}

label is how a participant is known: their participant ID (customerId) where the portal named them, else their address, else their webhook's host, with "(not confirmed)" after one still waiting. pendingWebhook (url, at) is a webhook given in the iframe and waiting for its owner to confirm it by email, and contactEmail is where their confirmations go. channelIds and feeds are the channels they receive of their group now. source is self for a subscribe in the iframe and owner for one the workspace added; blocked says whether the workspace blocked them, and since when. reachable says whether anything can reach them now, and lastDelivery is the newest line of the log for them (state, mechanism, event, at, error).

PATCH /v1/syndication/partners/{id} changes only the fields it is given:

FieldWhat it does
pausedNothing is sent while paused, and nothing waits for later.
blockedNothing is sent, the same address, webhook or participant ID cannot subscribe in the group again, and they do not count against the allowance. false unblocks.
customerIdTheir participant ID, the workspace's own, one per group; null clears it.
channelIdsTheir channels, of the group's, at least one where the group has several.
emailAnother address gets an email asking its owner to confirm it, and the answer says so in pendingEmail; the confirmed address keeps receiving until then. null stops the emails.
webhookUrlAnother webhook is called with a test at once, the answer carrying test, and gets posts once it answers with a 2xx. null removes it (so does webhook: null).
resendInvitetrue sends the email asking them to confirm their address again, to the address still waiting for confirmation, and the answer names it in pendingEmail. At most one a minute and five a day per participant; a 409 where no address waits or the participant is blocked.

An address or webhook another participant of the group has, or a blocked one, is refused with 409, and so is another's participant ID. Where the confirmation email could not go just now, the answer carries letterProblem with the reason, and the rest of the change is kept all the same. POST /v1/syndication/partners/{id}/delete removes a participant for good, with their lines in the log, and they can subscribe again. Removing a blocked participant ends the block with them, so block somebody who should stay out rather than remove them.

POST /v1/syndication/groups/{id}/partners adds a participant on the workspace's behalf, with the same fields as the iframe: channelIds (where the group has several), email and/or webhookUrl as the group allows, and an optional participant ID, customerId. It answers 201 with the partner (source owner), pendingEmail where a confirmation email went out (an address is confirmed by its owner, whoever added it), letterProblem where it could not go, test where a webhook was called, and, with a webhook, webhookSecret: the group's signing secret, once, for the workspace to pass on. The same address, webhook or participant ID already in the group is refused with 409, and so is a blocked one, and so is anybody new once the allowance is reached.

What a participant receives​

Every hand-over of a channel a participant takes is passed on to them, by the ways their group allows, whichever ways the channel hands over to you by, or none. A channel that asks for approval passes a post on once it is approved. An update or a delete goes only to the participants who were given that post; a resend of a post to your own people is not passed on. A paused participant is sent nothing. If a workspace has more participants than its allowance, the oldest are served first.

To pass a post on again, POST /v1/jobs/{id}/resend-participants (posts:write) sends it, as it reads now, to every participant whose group takes the channel; a 409 where no group with somebody subscribed takes it. See Sending a post again.

By email​

An email in your branding: "Your video is ready to post", "This post changed", "Please take this post down", with a button to the participant's post kit. The kit has the video to download or share, its cover and the text with a copy button, and nothing to report with. Its link works for 14 days. Every email ends on a link to stop them, which is also its List-Unsubscribe header. Every email a participant gets, the confirmations included, names the broadcast in a box headed Broadcast under its first line (participants join a broadcast and leave it: no page or email speaks of a subscription): the group's pageTitle (or "Ready-to-post videos from" and the branding's title) and the channels it is about, each with its platform, shape and language; the email asking to confirm joining also carries the group's description.

By webhook​

The same document as your own webhook call (Webhooks), with the same headers, signed the same way with the group's secret, and:

  • channels holds only the channels the participant takes;
  • deliveryId is the participant's own, and attempt counts their retries;
  • syndication says it was passed on: { "partnerId": "...", "from": "Acme Markets" };
  • on a publish, omitted names only channels they take;
  • who approved a post (approvedBy, approvedAt), the post list or the stream it came from, a manual post's title, and the channels they do not take are left out.

postId identifies a post across a later post.update or post.delete; a participant cannot report against it. The test call is a ping with the same syndication.

A call that is not answered with a 2xx within five seconds is tried again after 10 seconds, a minute, 5 minutes, 15 minutes, an hour and two hours, then gives up; an email the mail server refuses climbs the same ladder. A call answered with 410 Gone takes the webhook off, and its line reads SKIPPED; a participant it leaves with no way at all is then removed by the hourly sweep, with their lines.

Branding and email settings​

GET /v1/syndication/settings answers what is saved (look and sender, null where the default stands), what that comes to (effective), the defaults, the sending domain and the allowance. sender also says how the emails go:

{
"sender": {
"name": null,
"local": null,
"replyTo": "desk@acme-markets.com",
"mode": "smtp",
"smtp": {
"host": "mail.acme-markets.com",
"port": 587,
"security": "starttls",
"user": "no-reply@acme-markets.com",
"from": "no-reply@acme-markets.com",
"password": true
}
}
}

password only says whether one is kept: no route answers the password itself.

PUT /v1/syndication/settings saves any of these; null puts one back to its default:

FieldWhat it sets
titleBeside the logo at the top, up to 80 characters. Default: the workspace's name.
footerUnder the card, up to 600 characters. Line breaks are kept.
logomedia://<id> of a PNG, JPEG or GIF in the media library, at most 512 KB.
background, card, accent, signatureColours as #RRGGBB: the page, the card, buttons and links, the title and headings.
fontThe font of every group's page and of the pages the emails open, { font, size, file }: font one of the preinstalled fonts a brand can use (such as inter; open-sans where left out), size from 0.8 to 1.4 (default 1), and file a font file from the media library (media://<id>, WOFF2, WOFF, TTF or OTF) drawn instead. null is the usual one. It draws the pages and the emails; a mail app that loads no web fonts shows the nearest one it has.
senderNameThe From line's name, up to 60 characters, without ", <, >, @ or \. Default: the title.
senderModeHow the emails go: reelwire, from an address on Reelwire's sending domain through Reelwire's mail service (the default), or smtp, through the workspace's own mail server. One or the other.
senderLocalWith reelwire: the part before the @ of the sending address, on Reelwire's sending domain: 2 to 40 lower case letters, digits, dots and hyphens, starting and ending on a letter or digit. No mailbox stands behind it. Reelwire's own addresses (support, billing, no-reply and the like) and anything with "reelwire" in it are refused, and one another workspace sends from is a 409. Default: Reelwire's no-reply address.
smtpHostWith smtp: the mail server, such as mail.example.com. It must be on the internet: a name or address inside a private network is refused.
smtpPortWith smtp: 587, 465, 25 or 2525, the ports mail is sent on.
smtpSecurityWith smtp: starttls (usual with 587), tls (465) or none.
smtpUser, smtpPasswordWith smtp: the mail account's login. The password is kept encrypted; an empty or absent one keeps the stored one.
smtpFromWith smtp: the address the emails come from, one the account may send as.
replyToWhere a participant's reply goes: an address that can receive mail.

With smtp, every one of its fields must be set, and the server is tried before anything about it is saved: one that does not answer or does not take the login is a 400 quoting the server, and nothing is saved.

Every other colour follows from the four: words are dark or light by what they sit on. The same branding is worn by every group's iframe, every email and the post kit, and none of them carries Reelwire's name, except where neither logo nor title is set: then Reelwire's logo and name stand at the top (effective.house is true), and the texts use the workspace's name.

POST /v1/syndication/preview draws what a participant sees, in the saved branding with anything in draft laid over it:

kindWhat it draws
post (the default)The email that a post is ready: its subject, from, replyTo, html and text.
confirmThe email that asks a new address to confirm, in the same fields.
kitThe post kit an email opens, as html: built from the newest post of the group's channels where there is one (sample: false), else from a sample.
pageA group's sign-up page, as html, with its buttons switched off and a stand-in where the signing secret goes.

groupId names the group whose channels and ways it shows. group (pageTitle, description, channelIds, byEmail, byWebhook, showHeader) lays unsaved values over that group's, or stands in for a group not made yet, which is how the group dialogue's What participants see shows the page, an email and the post kit before anything is saved.

POST /v1/syndication/sender/test sends one test email in the branding, the way the saved settings say with anything in draft laid over them (the same fields as PUT), so a mail server can be tried before it is saved. It goes to the person signed in, or with an API key to the workspace's owner, and to nobody else. It answers { "ok": true, "to": "...", "via": "smtp", "state": "sent", "error": null }: state is sent, skipped where that address cannot receive mail, or failed, with the mail server's own words in error. Ten a minute.

The log​

GET /v1/syndication/deliveries lists every email (EMAIL) and webhook call (WEBHOOK) to a participant, newest first, as the Syndication log card on the Syndication screen shows it: partnerLabel (their participant ID, address or webhook host), group, event, channels, state (PENDING, DELIVERING, DELIVERED, RETRYING, DEAD, or SKIPPED where the way was gone by the time it was due), attempt, where it went (target) and what came back (lastStatusCode, lastError). It takes partnerId, groupId, state, mechanism, event, q, ascending, page and perPage (50 when left out, up to 200), and answers total, all (every line, unfiltered) and facets, the values each filter can take.

POST /v1/syndication/deliveries/{id}/redeliver sends a line again from its first attempt: an email with a fresh post kit link, a call with its clip links signed afresh. A line still on its way is a 409.

Through an AI assistant​

Everything on this page is a tool of the syndication set, listed at /mcp/all and found from /mcp with reelwire_find_tools: reelwire_list_syndication_groups, reelwire_add_syndication_group, reelwire_update_syndication_group and reelwire_remove_syndication_group for the groups; reelwire_list_partners, reelwire_add_subscriber (adding a participant), reelwire_update_partner and reelwire_remove_partner for the participants; reelwire_syndication_settings, reelwire_set_syndication_look, reelwire_syndication_preview and reelwire_syndication_test_email for the branding and the emails; reelwire_list_partner_deliveries and reelwire_resend_to_partner for the log. See Every tool.