Publishing to several social networks with one API call, shown with a real response
First published on rutba.io. Rutba Relay takes one post and a list of accounts, adapts the post to each network, publishes in the background and reports a separate result for every account. This article shows a real r
First published on rutba.io.
Rutba Relay takes one post and a list of accounts, adapts the post to each network, publishes in the background and reports a separate result for every account. This article shows a real request and response from that API, lists what a customer can publish to today, and compares it with Ayrshare and Hootsuite, whose pricing and documentation we read on 10 November 2026.
What a customer can publish to today
The catalogue at GET /v1/platforms lists 55 destinations, and Relay has an adapter for 16 of them. Five are open to any customer now, and a sixth is a test destination that reaches no network at all.
| Destination | How you connect it | What the adapter allows |
|---|---|---|
| Bluesky | Handle and app password | 300 characters, up to 4 images of 0.9 MB |
| Discord | Incoming webhook URL | 2,000 characters, up to 10 images or videos of 10 MB |
| Mastodon | Sign-in on your own instance | 500 characters, up to 4 images, or 1 video of 40 MB |
| Telegram | Channel name. Use our bot or your own, and make it an admin of the channel | 4,096 characters, up to 10 images of 10 MB or videos of 50 MB |
| WordPress | Self-hosted site URL, username and application password | No practical text limit, up to 10 images, native tags |
| Sandbox | Nothing to set up | For test suites. No network is contacted |
Read from the live catalogue on 10 November 2026.
The other ten adapters are written. Five of those networks can be connected within the limits each one sets until it has reviewed our app, and those limits decide whether a customer can use them. The other five cannot be connected, because no app is registered for them yet.
| Network | State on 10 November | What that means for a customer |
|---|---|---|
| Facebook Pages, Instagram, Threads | Connectable within Meta's limits. Our app stays in Development mode until Meta's review is done | Only people with a role on our app can connect, so customers cannot use these yet |
| TikTok | Connectable within TikTok's limits while it reviews our app, on its sandbox keys | Only named test accounts can connect, and posts stay private to the creator |
| YouTube | Connectable within Google's limits. Google has verified our app's data access, and its separate YouTube API audit has not passed yet | Every upload is forced private until that audit passes, so it is no use for publishing yet |
| LinkedIn, Pinterest, Reddit, Slack | No app registered yet | Cannot be connected |
| X | No app registered. X charges per request, so it will be sold against prepaid credit, and that is not in place | Cannot be connected |
If you build a picker from the catalogue
available: truemeans this deployment holds the keys for that network's app. Facebook reportstruebefore Meta's review has finished, so readintegration_statusas well.gatedmeans the network's own review still applies, andnotesays what it restricts.On the Agency plan an organisation can register its own app with a network through
/v1/org/platform-apps, and connections then use that app in place of ours. Our test suite covers that route with an invented Facebook app. It has not been proven with a customer's approved app, so talk to us before planning around it.
The run behind the examples
The requests and responses below come from one run on 10 November 2026: the Relay gate, its delivery worker and the public API door, running on a development machine and called with an API key. Every publish went to Sandbox accounts, so no social network was contacted. The previews used the real rules for Bluesky and Mastodon, with connection rows we inserted for the run, because a preview only adapts the post and never reaches a network.
The ids are the ones that run produced, and the responses are trimmed to the fields that matter. On the hosted service the base URL is https://api.relay.rutba.io and live keys begin rsk_live_. API keys and webhooks come with the Agency plan, listed at $99 a month, so the free plan covers the console and does not cover this walkthrough. Agency is sold by written quote today rather than self-serve checkout, through the Rutba Social page. Once a workspace is on it, the API key works immediately.
Connect an account
curl -X POST https://api.relay.rutba.io/v1/connections \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "content-type: application/json" \
-d '{ "platform": "sandbox", "fields": { "handle": "@first-account" } }'
{
"data": {
"id": "u0czr5buhlftcszrxuik6ant",
"platform": "sandbox",
"handle": "@first-account",
"display_name": "Sandbox account",
"status": "active"
}
}
Bluesky, Discord, Telegram and WordPress connect through this same call, each with its own fields, which the catalogue lists per network. Mastodon goes through a sign-in on your own instance. For an account that belongs to somebody else, POST /v1/connect-sessions makes a hosted link they can open themselves.
See what each network will get
A preview runs the same adaptation the publisher runs and writes nothing. This post is 372 characters long, which suits Mastodon and does not suit Bluesky.
curl -X POST https://api.relay.rutba.io/v1/posts/preview \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "content-type: application/json" \
-d '{
"content": { "text": "Version 4 of our invoicing app is out today. Recurring invoices can now be paused for a month without losing the schedule, statements export to CSV as well as PDF, and the slow customer search on accounts with more than 20,000 rows is fixed. The full list of changes is on the changelog, and the upgrade is automatic for every hosted account, so nobody has to do anything." },
"connection_ids": ["vg8ubpzn43y38uqxpki8wr3u", "qeiby35tu44xz6t0kjjt9adf"]
}'
Bluesky's part of the 200 response:
{
"connection_id": "vg8ubpzn43y38uqxpki8wr3u",
"platform": "bluesky",
"handle": "yourbrand.bsky.social",
"connection_status": "active",
"text": "Version 4 of our invoicing app is out today. Recurring invoices can now be paused for a month without losing the schedule, statements export to CSV as well as PDF, and the slow customer search on accounts with more than 20,000 rows is fixed. The full list of changes is on the changelog, and theβ¦",
"warnings": [
{
"code": "too_long",
"field": "text",
"message": "Bluesky allows 300 characters, got 372",
"fixable": true
}
],
"blocking": [],
"publishable": true
}
The text was cut at a word boundary to 296 characters, and the warning says why. Mastodon's copy came back whole at 372 with no warnings. Send "validation": "strict" and nothing is adapted: in the same run Bluesky came back with "publishable": false and a too_long entry under blocking, and Mastodon stayed publishable.
On Bluesky, Mastodon, Telegram and Discord, Relay puts the link and hashtags in the body, so they count towards the limit. In lenient mode it shortens your words to make room and leaves the link and every hashtag whole. Given the same 372 characters with a link and two tags, the Bluesky rules cut the words to 246 characters and the post came to 297. The 192-character post in the next step comes to 243 with that link and those tags.
Publish to several accounts at once
One request, with an Idempotency-Key so that a retry cannot post twice. The third target uses a Sandbox override that asks for a refusal, so the response shows what a failure looks like.
curl -X POST https://api.relay.rutba.io/v1/posts \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "Idempotency-Key: release-4-announcement" \
-H "content-type: application/json" \
-d '{
"content": {
"text": "Version 4 of our invoicing app is out today. Recurring invoices can now be paused for a month, statements export to CSV as well as PDF, and the slow customer search on large accounts is fixed.",
"link": "https://example.com/changelog/4",
"tags": ["changelog", "saas"]
},
"targets": [
{ "connection_id": "u0czr5buhlftcszrxuik6ant" },
{ "connection_id": "dlhdpjmr5jr8wkbpxf1vg5q3" },
{ "connection_id": "rzesyqu5iao9x44oxbodhogm",
"overrides": { "outcome": "content_rejected" } }
]
}'
The answer is a 202, trimmed here to the post and its deliveries:
{
"data": {
"id": "gsvpcm1j1n4jerme5kxrmej7",
"status": "queued",
"source": "api",
"deliveries": [
{ "id": "tkcmunic8f9vgbkun4lz42gt", "platform": "sandbox",
"connection_id": "u0czr5buhlftcszrxuik6ant", "status": "pending", "attempts": 0 },
{ "id": "pmjmr51qfyj9pmzsx5xliv0x", "platform": "sandbox",
"connection_id": "dlhdpjmr5jr8wkbpxf1vg5q3", "status": "pending", "attempts": 0 },
{ "id": "oliszvo4p9bah2ouekd75dn8", "platform": "sandbox",
"connection_id": "rzesyqu5iao9x44oxbodhogm", "status": "pending", "attempts": 0 }
]
}
}
No network has been contacted at this point. The API has written one post and one delivery per target, and each delivery is queued on its own, so the call returns quickly however many accounts it names and a slow network cannot hold up the others.
Replace targets with "platforms": ["bluesky", "mastodon", "telegram"] to reach every active account on those networks. If one of them has no active connection, the request is refused with a 409 and connection_unavailable. Add scheduled_at and timezone to send the post later.
Read the result
curl https://api.relay.rutba.io/v1/posts/gsvpcm1j1n4jerme5kxrmej7 \
-H "Authorization: Bearer $RELAY_API_KEY"
{
"data": {
"id": "gsvpcm1j1n4jerme5kxrmej7",
"status": "partial",
"published_at": "2026-11-10T04:29:10.419Z",
"deliveries": [
{
"id": "tkcmunic8f9vgbkun4lz42gt",
"platform": "sandbox",
"status": "succeeded",
"attempts": 1,
"remote_id": "sbx_d4c45fa83f129e36b5c0f119",
"remote_url": "https://sandbox.rutba.io/p/sbx_d4c45fa83f129e36b5c0f119",
"error": null,
"error_code": null
},
{
"id": "pmjmr51qfyj9pmzsx5xliv0x",
"platform": "sandbox",
"status": "succeeded",
"attempts": 1,
"remote_id": "sbx_f490bd67448b7d2ddee4b8ba",
"remote_url": "https://sandbox.rutba.io/p/sbx_f490bd67448b7d2ddee4b8ba",
"error": null,
"error_code": null
},
{
"id": "oliszvo4p9bah2ouekd75dn8",
"platform": "sandbox",
"status": "rejected",
"attempts": 1,
"remote_id": null,
"remote_url": null,
"error": "Sandbox: the post was refused on request",
"error_code": "content_rejected"
}
]
}
}
Two accounts took the post and one refused it, so the post as a whole is partial. The refused delivery shows one attempt, because a refusal is final. Rate limits and outages are retried, each delivery on its own budget. GET /v1/deliveries?post_id=β¦ returns the same rows with filters and a cursor, and POST /v1/deliveries/{id}/retry sends one destination again, which Relay refuses for a delivery the network has rejected.
Send it twice on purpose
A timeout leaves you not knowing whether the post went out, so the safe move is to send the same request again. With the same Idempotency-Key and body, the run returned the original post id and a response header of idempotent-replay: true. Keys are remembered for 24 hours. The same key with a different body is refused with a 409:
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key \"release-4-announcement\" was already used with a different request body"
}
}
Hear about it by webhook
This post produced five events at the test receiver: post.created, delivery.failed, delivery.succeeded twice and post.partial. Do not build on their order. In an earlier run the same day, post.partial arrived before the second delivery.succeeded.
{
"id": "evt_cw8G-dyyQXy0ZPA2",
"type": "post.partial",
"created_at": "2026-11-10T04:29:10.460Z",
"data": {
"post_id": "gsvpcm1j1n4jerme5kxrmej7",
"status": "partial",
"succeeded": 2,
"failed": 1,
"external_id": null,
"deliveries": [
{ "id": "tkcmunic8f9vgbkun4lz42gt", "platform": "sandbox", "status": "succeeded",
"remote_url": "https://sandbox.rutba.io/p/sbx_d4c45fa83f129e36b5c0f119",
"error": null, "error_code": null },
{ "id": "pmjmr51qfyj9pmzsx5xliv0x", "platform": "sandbox", "status": "succeeded",
"remote_url": "https://sandbox.rutba.io/p/sbx_f490bd67448b7d2ddee4b8ba",
"error": null, "error_code": null },
{ "id": "oliszvo4p9bah2ouekd75dn8", "platform": "sandbox", "status": "rejected",
"remote_url": null,
"error": "Sandbox: the post was refused on request", "error_code": "content_rejected" }
]
}
}
Each request carries x-relay-signature: t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of the timestamp and the raw body joined by a full stop. A retry of the same event keeps the same x-relay-delivery id, so you can deduplicate on it.
Against Ayrshare and Hootsuite
Start with what counts against us. Ayrshare and Hootsuite already publish to Instagram, TikTok and LinkedIn for their customers, and Relay cannot yet. If one of those networks is the reason for your integration, that settles it for now.
| Rutba Relay | Ayrshare | Hootsuite | |
|---|---|---|---|
| What it is | A publishing API with a web console on the same routes | A publishing API for developers, with a light dashboard | A social media management suite, with a REST API beside it |
| Lowest listed price with the API | $99 a month (Agency), by written quote, no cap on accounts or seats | $149 a month (Premium), one profile with up to 14 accounts | Its plans page does not say which plans include the API. Standard starts at $99 per user a month on annual billing, with 10 accounts |
| Growing | Agency has no account or seat cap | $299 a month for up to 10 profiles, $599 for 30, then a per-profile rate | Professional starts at $199 per user a month, with unlimited accounts |
| Trying it | A free plan for the console, without the API | A 28-day trial on Launch, no free plan | A 14-day trial |
| Open to customers today | Bluesky, Discord, Mastodon, Telegram, WordPress | Bluesky, Facebook, Google Business, Instagram, LinkedIn, Pinterest, Reddit, Snapchat, Telegram, Threads, TikTok, X, YouTube | Instagram, TikTok, LinkedIn and the other large networks, plus Bluesky |
| The call |
POST /v1/posts answers 202, then one delivery per account |
POST /api/post returns a postIds entry per platform |
POST /v1/messages takes socialProfileIds and a send time at least five minutes ahead |
| Test destination | Sandbox accounts that refuse, rate-limit or lose a response on request | Parameters such as randomPost for test content. We found no separate sandbox in its docs |
None in the guides we read |
| Repeated requests |
Idempotency-Key, kept for 24 hours, replays the first answer |
Optional idempotencyKey, duplicates are rejected |
Not covered in its scheduling guide |
| Inbox and listening | Neither | A messaging API for Instagram, Facebook, WhatsApp and X | Inbox, listening and benchmarking |
Read on 10 November 2026 from ayrshare.com/pricing, ayrshare.com/docs, hootsuite.com/plans and developer.hootsuite.com. Prices in US dollars.
Our prices are marked as indicative while plans are being finalised, and a quote is confirmed in writing before anything is charged.
Which one fits
- Hootsuite if a marketing team needs to answer comments and watch mentions from one screen. Relay has no inbox and no listening.
- Ayrshare if your product needs Instagram, TikTok, LinkedIn or X this month, or if you post for thousands of your own customers and want a per-profile price.
- Relay if your destinations are Bluesky, Mastodon, Telegram, Discord or WordPress, if somebody who does not write code also needs a console, or if you want your test suite to exercise refusals and rate limits on demand. Ayrshare's publish endpoint lists Bluesky and Telegram, and it does not list Mastodon, Discord or WordPress.
Every endpoint, field and plan limit is on relay.rutba.io, and the platforms page shows the state of each network. Read the API reference.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.