← All posts
Articles

Why Per-Platform Results Matter

Publishing to many networks at once means some will fail while others succeed. Here is how to model partial failures, retries and idempotency so your users always know what happened.

Relaycast Team · April 21, 2026 · 4 min read

When you publish one post to five networks, you have not made one request. You have made five, to five different systems, each with its own uptime, rate limits and rules. Sooner or later one of them will fail while the other four succeed.

How your system represents that moment decides whether your users trust it. This article explains how Relaycast models results per platform, and what that means for retries and duplicate protection.

A post is not one thing

The tempting model is a single post with a single status: it either worked or it did not. That model breaks the first time LinkedIn accepts a post and Instagram rejects the image. Is the post "published"? "Failed"? Neither answer is true, and either one leads to a bad decision. Report "failed" and the user retries, double-posting on LinkedIn. Report "published" and nobody notices Instagram is missing.

Relaycast splits every post into targets, one per account. Each target is published by its own job and has its own result:

  • status: queued, publishing, published or failed
  • externalPostId and externalUrl: where the post lives on the network
  • errorCode and errorMessage: why it failed, if it did
  • attempts: how many times publishing was tried
  • publishedAt

The post's overall status is derived from its targets:

Post statusMeaning
scheduledWaiting for its scheduled time.
publishingAt least one target is still queued or in progress.
publishedEvery target published.
partialEvery target finished, with a mix of successes and failures.
failedEvery target failed.

partial is the important one. It tells you, in one word, that a human may need to look, and the targets tell you exactly where.

Fail early when you can

The cheapest failure is one that never gets queued. Relaycast validates each target against its platform's rules when you create the post: character limits, media counts and types, option values. If any target is invalid, the whole request returns 400 validation_error, with issues grouped by account ID:

{
  "error": {
    "code": "validation_error",
    "message": "Content is not valid for some platforms",
    "details": {
      "ACCOUNT_ID": [{ "field": "content", "message": "Bluesky allows 300 characters (this post has 342)" }]
    }
  }
}

Nothing is published and no credits are spent. This is deliberate: if you asked for five networks and one cannot work, it is usually better to fix the request than to publish to four and leave a gap.

Validation catches what can be known in advance. It cannot catch a network being down, a token revoked five minutes ago, or a video the platform decides it cannot process. Those surface as target-level failures after publishing starts.

Retries: automatic, but not blind

Not every error deserves a retry. A timeout or a temporary platform error probably will succeed later. A caption that is too long or a revoked token will fail every time.

Each failure inside Relaycast is classified as retryable or not:

  • Retryable errors put the target back in the queue, and the job is retried with exponential backoff. While it waits, the target shows the most recent error so you can see what is going on.
  • Non-retryable errors, or a retryable one on its last attempt, mark the target failed straight away.
  • If the platform reports that access was revoked, the account is marked reconnect_required so the problem is visible in the dashboard and in GET /v1/accounts.

Failed targets are refunded: you are charged one credit per target when the post is created, and that credit comes back if the target ends up failing.

There is one more subtle case. Some networks accept a post, then process it in the background. If Relaycast has handed the post over but the network has not confirmed it yet when polling times out, retrying would risk publishing a duplicate. In that situation the target is reported as published without a final URL, rather than retried.

Your retries: idempotency keys

Automatic retries cover the platform side. Your side has the same problem: your server sends POST /v1/posts, the connection drops, and you do not know whether the post was created.

Send an Idempotency-Key header with every create request:

curl -X POST "$RELAYCAST_API_URL/v1/posts" \
  -H "x-access-key: $RELAYCAST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: campaign-42-launch" \
  -d '{ "content": "Launch day!", "targets": [{ "accountId": "ACCOUNT_ID" }], "publishNow": true }'

If a post with that key already exists in your project, the API returns the original post with 200 OK instead of creating a new one (a fresh post returns 201 Created). Two concurrent requests with the same key also resolve to a single post. Retrying is now always safe.

Good keys are derived from something meaningful in your system, such as the ID of the scheduled item or campaign, so that a retry naturally reuses it.

Handling a partial result

Once a post is partial, the useful question is "what now?" A pattern that works well:

  1. Read the post with GET /v1/posts/{id}.
  2. Collect targets with status: "failed".
  3. Show the user which accounts failed and why, using errorMessage.
  4. If the cause is fixable (shorter caption, different media, reconnected account), create a new post that targets only those accounts.
const { data: post } = await fetch(`${API}/v1/posts/${postId}`, { headers }).then((r) => r.json());
const failed = post.targets.filter((t) => t.status === "failed");

for (const t of failed) console.log(`${t.platform}: ${t.errorMessage}`);

if (failed.length) {
  await fetch(`${API}/v1/posts`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": `${postId}-retry-1` },
    body: JSON.stringify({
      content: post.content,
      media: post.media,
      targets: failed.map((t) => ({ accountId: t.accountId, content: t.content ?? undefined, options: t.options })),
      publishNow: true,
    }),
  });
}

Targeting only the failed accounts is what keeps the successful networks from getting a duplicate.

Don't poll forever: subscribe

Instead of polling, set up a notification rule under Settings → Notifications. The post.partial and post.failed events fire when a post reaches that state, by email or as a signed webhook that includes every target's status, URL and error. See notifications for the payload and how to verify signatures.

The takeaway

Multi-network publishing is a distributed system, whether you think of it that way or not. Modelling each platform as an independent result, validating early, retrying only what can succeed, and making your own retries idempotent turns "it sometimes double-posts" and "we did not notice Instagram failed" into problems you simply do not have.

#architecture#reliability#idempotency

Ready to build?

Get an access key and publish your first post in minutes.

Related posts