Skip to content
Rutba Relay
Get started

API reference

The Rutba Relay API

One API to publish a post to many social platforms. Send a neutral post plus a list of targets; the Relay adapts it per platform, publishes asynchronously, and reports each delivery separately. Authenticate with an API key from the Relay console: `Authorization: Bearer rsk_live_…`. A key carries scopes (read, publish, connect, admin) and belongs to one organization. API access is part of some plans — `GET /v1/plans` lists which. Errors are `{ "error": { "code", "message", "details" } }` with a lowercase code; successful answers are `{ "data", "meta" }`.

Base URL
https://api.relay.rutba.io
Document
version 0.2.0 · 57 operations · openapi.json
Concentric circles crossed by fine ruled lines, like a calibration target

Conventions

Authenticate with Authorization: Bearer <key> or an X-API-Key header. Operations marked public need no key.

A success is { data, meta }; a failure is { error: { code, message, details } }. Branch on the code.

Send Idempotency-Key when creating. A repeat of the same request returns the original response.

Publishing answers 202 Accepted with one delivery per destination; each settles on its own.

One POST /v1/posts returns one post and one delivery per destination. Each is reported and retried on its own, which is why deliveries are a resource of their own below rather than a field on the post. The error vocabulary and webhook signatures are on the developers page.

One form on the left joined by thin lines to four impressions of it on the right

Who is calling

Who a key belongs to

GET/v1/auth/meThe signed-in user, their organisations, and their effective scopes

The one call the portal makes on every page load. Shape is stable — the portal builds on it.

Link to this operation

Platforms

What the relay can publish to

GET/v1/platformspublicList every destination and its status

Parameters

statusquery"open" | "gated" | "no_api" | "read_only"
categoryquerystring
availablequerybooleanOnly platforms connectable right now

Link to this operation

Connections

Authorised accounts

GET/v1/connect-sessionsList connect links

Outstanding first. Tokens are never returned — a link is shown once, at mint.

Parameters

statusquery"pending" | "used" | "expired"

Link to this operation

POST/v1/connect-sessionsMint a connect link

Returns a single-use URL that lets somebody outside the organisation authorise one account. The link carries no API key and can do nothing else. Send it to the person who holds the platform credentials.

Link to this operation

DELETE/v1/connect-sessions/{id}Revoke a connect link

Kills a link that has not been used yet — the one thing you need when a link went to the wrong chat.

Parameters

id*pathstring

Link to this operation

GET/v1/connectionsList connections

Parameters

platformquerystring
statusquery"active" | "reauth_required" | "disabled" | "revoked"

Link to this operation

POST/v1/connectionsConnect an account with a token, app password, or webhook URL

For platforms whose auth.kind is not oauth2. The required fields are listed by GET /v1/platforms. OAuth platforms use POST /v1/oauth/{platform}/start instead.

Link to this operation

PATCH/v1/connections/{id}Update a connection

Metadata is merged, not replaced. This is how per-connection settings are set — e.g. {"metadata":{"channel":"#general"}} for Slack.

Parameters

id*pathstring

Link to this operation

DELETE/v1/connections/{id}Delete a connection

Deletes the stored credentials. Deliveries already published stay in history; pending ones for this connection are cancelled.

Parameters

id*pathstring

Link to this operation

POST/v1/connections/{id}/verifyCheck the credentials still work

Calls the platform. Refreshes the cached handle and display name, and flags the connection if the platform has stopped accepting it.

Parameters

id*pathstring

Link to this operation

POST/v1/oauth/{platform}/startBegin an OAuth connect

Returns an authorization URL to send the account owner to. The connection appears once they approve and the platform redirects back.

Parameters

platform*pathstring

Link to this operation

Posts and deliveries

Publishing

GET/v1/deliveriesThe delivery log across every post

One row per (post, destination), newest first. Filterable by platform, status, post, connection and date, and keyset paginated with cursor.

status and platform accept comma-separated lists — "which Instagram and Threads deliveries failed this week" is one request, not two.

Parameters

platformquerystringPlatform id, or several comma-separated.
statusquerystringpending, processing, succeeded, failed, rejected, skipped, cancelled — or several comma-separated.
post_idquerystring
connection_idquerystring
fromquerystring (date-time)
toquerystring (date-time)
limitqueryinteger
cursorquerystringA delivery id from the previous page.

Link to this operation

POST/v1/deliveries/{id}/retryRetry one destination

Requeues a single failed delivery, leaving every other destination of the same post alone — including ones that succeeded, which is the whole point: eight platforms took the post and Reddit was rate-limited.

A rejected delivery is not retried. The platform refused the content, and sending the same bytes again fails identically; edit the post instead.

Parameters

id*pathstring

Link to this operation

GET/v1/postsList posts

Parameters

statusquerystring
external_idquerystring
limitqueryinteger
cursorquerystringA post id from the previous page.

Link to this operation

POST/v1/postsPublish a post to one or more connections

Send content plus targets. Targets can be given three ways: targets (with per-target overrides), connection_ids, or platforms (every active connection on those platforms).

Returns 202 with a post id and one delivery per target. Deliveries settle independently — watch them with webhooks or GET /v1/posts/{id}.

Send an Idempotency-Key header. A repeat of the same request returns the original response instead of publishing twice.

Parameters

idempotency-keyheaderstringUnique per logical publish. Remembered for 24 hours.

Request body · application/json

contentobject
content.textstring
content.titlestringUsed by platforms with a real title field (Reddit, WordPress).
content.linkstring (uri)
content.tagsstring[]
content.mediaobject[]
content.media[].idstringAn asset from POST /v1/media
content.media[].urlstring (uri)Or a public URL we fetch
content.media[].altstring
targetsobject[]
connection_idsstring[]
platformsstring[]
overridesobjectPer-platform extras, keyed by platform id.
scheduled_atstringAn ISO instant ("2026-08-17T04:00:00Z"), or a local wall clock ("2026-08-17T09:00") read in timezone.
timezonestringIANA zone for a wall-clock scheduled_at, e.g. "Asia/Karachi". Defaults to the organisation's. Ignored when scheduled_at carries its own offset.
validation"strict" | "lenient"lenient (default) adapts the post per platform — truncating text, dropping media a platform will not take. strict refuses instead.
external_idstring
draftboolean

Link to this operation

PATCH/v1/posts/{id}Edit a draft or a scheduled post

Drafts and scheduled posts only. A published post cannot be edited — the relay cannot reach into fifteen networks and change what is already there — so it is refused with a pointer at POST /v1/posts/{id}/duplicate.

The post is re-projected against every target, so a target that was refused at validation gets another chance: the edit may be the fix. Changing the targets rewrites the deliveries; leaving them out keeps the ones there are.

scheduled_at: null unschedules, turning a scheduled post back into a draft.

Parameters

id*pathstring

Request body · application/json

contentobject
targetsobject[]
connection_idsstring[]
platformsstring[]
overridesobject
scheduled_atstring | null
timezonestring
external_idstring | null
validation"strict" | "lenient"

Link to this operation

DELETE/v1/posts/{id}Cancel the deliveries not yet sent

Only pending deliveries are cancelled. Anything already published stays published — the relay cannot un-post.

Parameters

id*pathstring

Link to this operation

GET/v1/posts/{id}/approvalsEvery review round on a post

Newest first. A row with no decided_at is an open review. "Rejected twice before it went out" is a question this answers and a single status column cannot.

Parameters

id*pathstring

Link to this operation

POST/v1/posts/{id}/duplicateCopy a post as a new draft

Reposting is the most common thing a customer does after publishing. The copy is a draft aimed at the same accounts, and nothing is sent until you call publish or schedule.

Targets whose connection is no longer usable are dropped and listed in dropped_targets — a duplicate of a post from June should not look ready to send to an account that was revoked in July.

external_id is not copied: it names one record in your system, and two posts claiming it would break every lookup by it.

The body is optional — a bare POST duplicates as a plain draft. It may carry external_id, source, scheduled_at and timezone.

Parameters

id*pathstring

Link to this operation

GET/v1/posts/{id}/metricsEngagement for one post, per delivery and totalled

The most recent reading collected for each of the post's deliveries.

Every field is nullable and a null is not a zero: platforms report different things, and most report no impressions at all. A total is absent when no platform supplied that number, rather than being summed as zero — "nobody counted" and "nobody saw it" are different answers.

Readings are collected on a schedule that decays with the post's age and stops after a week, so next_collection_at is null for an older post and collected_at is when the platform was last asked, not when it counted.

Parameters

id*pathstring

Link to this operation

POST/v1/posts/{id}/publishRelease a draft now

Queues every pending delivery immediately. A scheduled post can be released early this way — its delayed jobs are removed first, so it goes out once rather than now and again at the time nobody wants any more.

Targets refused at validation stay refused: publishing is not a retry, and sending content a platform already rejected would fail identically.

Parameters

id*pathstring

Link to this operation

POST/v1/posts/{id}/retryRetry the failed deliveries of a post

Requeues deliveries in failed state. Deliveries in rejected state are not retried — the platform refused the content, and sending it again would fail identically.

Parameters

id*pathstring

Link to this operation

POST/v1/posts/{id}/scheduleSet or move a post's scheduled time

Works on a draft and on an already-scheduled post: the delayed job is removed and re-added, because BullMQ ignores an add for a job it already holds and the time would otherwise silently not move.

scheduled_at takes an ISO instant ("2026-08-17T04:00:00Z") or a local wall clock ("2026-08-17T09:00") read in timezone, then yours, then the organisation's. When the clocks change under that local time the response carries a schedule_notice saying how it was resolved.

A delivery already being published is left exactly as it is and reported in skipped — it is too late to move a post that is going out right now.

Parameters

id*pathstring

Request body · application/json

scheduled_at*string
timezonestringIANA zone, e.g. "Asia/Karachi".

Link to this operation

POST/v1/posts/{id}/submitSubmit a post for review

Moves a draft or scheduled post to pending_approval, where it cannot be published until somebody decides. Submitting twice leaves one open review rather than two.

Parameters

id*pathstring

Link to this operation

GET/v1/posts/calendarScheduled posts in a date range

from and to are local calendar dates (YYYY-MM-DD) in the requested timezone, and the range covers whole local days — a report for "16 August" in Karachi starts at 19:00 UTC on the 15th, and one that ignored that would be wrong by five hours at both ends.

Parameters

from*querystringLocal date, YYYY-MM-DD.
to*querystringLocal date, YYYY-MM-DD. Inclusive.
statusquerystringComma-separated post statuses. Defaults to scheduled and queued.
timezonequerystringIANA zone. Defaults to yours, then the organisation's.
limitqueryinteger

Link to this operation

POST/v1/posts/previewSee what a post becomes on each platform, without publishing

Runs the same projection the worker publishes through, against the same connections, and writes nothing: no post, no deliveries, no queue job, no metered usage, no media fetch.

Returns per target the text as that platform would receive it, which media it will not take and why, and every warning. This is what a composer draws.

Media given as a url is not downloaded — its type is assumed from the URL and its size is not checked. Those refs come back in unverified_media so the preview never claims to have looked at something it has not.

Request body · application/json

contentobject
targetsobject[]
connection_idsstring[]
platformsstring[]
overridesobject
validation"strict" | "lenient"

Link to this operation

Media

Uploads

GET/v1/mediaList uploaded assets

Parameters

kindquery"image" | "video" | "audio"
limitqueryinteger
cursorquerystring

Link to this operation

POST/v1/mediaUpload an image or video

multipart/form-data with a file part, and optionally alt. Returns an asset id to use in a post’s media array. The same bytes uploaded twice by one organization return the same asset. The largest file accepted is the plan’s media limit (GET /v1/billing).

Link to this operation

DELETE/v1/media/id/{id}Delete an asset

Removes the asset. The stored file goes when no other asset of this organization refers to it; posts already published keep what the platform holds.

Parameters

id*pathstring

Link to this operation

Templates

Reusable content presets

GET/v1/templatesList templates

Parameters

limitqueryinteger
cursorquerystringA template id from the previous page.

Link to this operation

POST/v1/templatesSave a template

content takes the same shape as a post's, and default_targets the same selectors — so applying a template is a spread into POST /v1/posts rather than a translation.

Request body · application/json

name*string
content*object
default_targetsobject
default_targets.platformsstring[]
default_targets.connection_idsstring[]
overridesobjectPer-platform extras, keyed by platform id.

Link to this operation

PATCH/v1/templates/{id}Update a template

Parameters

id*pathstring

Request body · application/json

namestring
contentobject
default_targetsobject
overridesobject

Link to this operation

Webhooks

Event delivery

POST/v1/webhooksRegister a webhook endpoint

The signing secret is returned once and cannot be read back.

Each request carries X-Relay-Signature: t={unix},v1={hmac} where the HMAC is SHA-256 over {t}.{raw body} keyed with the secret. Verify it and reject a timestamp older than five minutes.

Link to this operation

GET/v1/webhooks/{id}/deliveriesRecent delivery attempts for this endpoint

Parameters

id*pathstring

Link to this operation

Analytics

Aggregates over deliveries

GET/v1/analytics/platformsSuccess rate per platform

One row per destination, worst success rate first — the ordering a customer is looking for, because the reason to open this page is that something is broken.

Parameters

fromquerystringLocal date (2026-08-01) or ISO instant. Defaults to 30 days ago.
toquerystringLocal date or ISO instant. Defaults to now.
timezonequerystringIANA zone. Defaults to yours, then the organisation's.

Link to this operation

GET/v1/analytics/summaryHeadline numbers for a period

Deliveries by status, posts by status, and the success rate over them.

from and to take either a local calendar date (YYYY-MM-DD, resolved to whole days in timezone) or an ISO instant. Defaults to the last 30 days.

Parameters

fromquerystringLocal date (2026-08-01) or ISO instant. Defaults to 30 days ago.
toquerystringLocal date or ISO instant. Defaults to now.
timezonequerystringIANA zone. Defaults to yours, then the organisation's.

Link to this operation

GET/v1/analytics/timeseriesDeliveries over time, by status

Buckets are local to timezone, so a daily chart breaks at the customer's midnight rather than at UTC's — five hours out for a Karachi tenant, which is enough to move an evening campaign into the wrong day.

Empty buckets are filled in. A chart that skips a day with no deliveries draws a line straight through it and hides the outage.

Parameters

fromquerystring
toquerystring
timezonequerystring
granularityquery"hour" | "day" | "week" | "month"

Link to this operation

Organisation

The organization a key acts for, and its people

GET/v1/org/platform-appsThis organisation's own platform apps

The OAuth apps this organisation connects through instead of the shared one. Client secrets are never returned. supported lists the platforms that can be given an app today.

Link to this operation

POST/v1/org/platform-appsConfigure an app for a platform

Replaces any existing app for the same platform. The secret is encrypted at rest and never returned.

Link to this operation

PATCH/v1/org/platform-apps/{id}Rotate, relabel or switch off an app

Switching an app off falls back to the shared app for new authorisations. Connections already made through it keep working.

Parameters

id*pathstring

Link to this operation

DELETE/v1/org/platform-apps/{id}Remove an app

Connections authorised through it are kept and keep publishing; they can no longer refresh their tokens, so they will ask to be reconnected once the current one expires.

Parameters

id*pathstring

Link to this operation

Plans and usage

Plans and usage

GET/v1/billingThis organization’s plan, limits and usage this month

Read from the organization’s plan record on every request: how it is on the plan (a subscription, a licence, or assigned by Rutba), its limits (-1 is unlimited), the features it carries, and posts and deliveries used this month. A post counts once, when its first destination takes it.

Link to this operation

GET/v1/planspublicThe Relay’s plans and what each includes

The price list, read from the catalogue record rutba.io shows: each card’s name, price and words, and the Relay limits and features of the plan it sells. A price_minor of null means on request. A limits of null means the card sells no plan this API enforces (an enterprise contract). Answers without a credential.

Link to this operation

Rendered from the Relay’s OpenAPI document, version 0.2.0. Routes for sign-in, provisioning and other surfaces handled elsewhere in Rutba are not part of this reference.

One family

The rest of Rutba

One account across all of it. Sign in once and the products know each other.

RELAY-DOCS · 1f157db