# Cross-Post to 12 Channels in One API Call

The biggest reason to use OmniSocials instead of calling every platform's API directly is that a single `POST /posts/create` call can target any number of connected channels at once. You write one request, OmniSocials fans it out, handles each platform's quirks, and tells you what happened.

This guide walks through the patterns that matter when you're posting the same idea to multiple platforms at the same time.

## The one-request-many-platforms mental model

Every post has an `accounts` array. Put as many account IDs in it as you want, and OmniSocials publishes the same post to each of them.

```bash
curl -X POST https://api.omnisocials.com/v1/posts/create \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": { "default": "New feature launched today 🚀" },
    "accounts": [
      "your-facebook-account-id",
      "your-instagram-account-id",
      "your-linkedin-account-id",
      "your-linkedin-page-account-id",
      "your-x-account-id"
    ],
    "media_urls": ["https://example.com/launch.jpg"]
  }'
```

That single call produces one post record in OmniSocials and publishes it to Facebook, Instagram, both LinkedIn slots, and X. The response includes the resulting URLs for each platform once publishing completes.

## Same text everywhere (flat string)

The simplest shape. A single string in `content` is used on every channel in the `accounts` array.

```json
{
  "content": "New blog post is live: https://example.com/blog",
  "accounts": ["fb-id", "x-id", "linkedin-id"]
}
```

Use this when the message is short, generic, and reads well on every platform. The downside is there is no tuning per platform.

## Different text per platform

Pass an object keyed by channel ID. The `default` key is the fallback for channels that don't have an explicit entry.

```json
{
  "content": {
    "default": "We just shipped something cool. Check the blog for details.",
    "x": "New drop 🔥 https://example.com/blog",
    "linkedin": "Today we shipped a new capability. Here's what it unlocks for teams managing multi-platform content at scale...",
    "linkedin_page": "[Official] Our engineering team just shipped a major capability. Read the full write-up on our blog."
  },
  "accounts": [
    "your-facebook-account-id",
    "your-instagram-account-id",
    "your-x-account-id",
    "your-linkedin-account-id",
    "your-linkedin-page-account-id"
  ]
}
```

Facebook and Instagram fall back to the `default` copy. X gets a punchy version under its tighter character limit. The two LinkedIn channels each get their own voice: personal tone on the profile, official tone on the company page.

You only need to override the channels you actually want to customize. Everything else inherits `default`.

## Same media, different per-platform content

Media attachment works the same way: flat array (same on every channel) or object keyed by channel ID.

```json
{
  "content": {
    "default": "Behind the scenes from the photoshoot",
    "linkedin": "A look at the creative process behind our latest campaign. The full story is on our blog."
  },
  "accounts": [
    "instagram-id",
    "linkedin-id",
    "linkedin-page-id"
  ],
  "media_urls": [
    "https://example.com/photo1.jpg",
    "https://example.com/photo2.jpg",
    "https://example.com/photo3.jpg"
  ]
}
```

All three channels get the same three photos. LinkedIn channels get the custom copy, Instagram inherits `default`.

## Different media per platform

When platforms need different image aspect ratios or video formats, key the media object by channel ID. The `default` key is the fallback.

```json
{
  "content": { "default": "Spring collection is live" },
  "accounts": [
    "instagram-id",
    "pinterest-id",
    "facebook-id",
    "x-id"
  ],
  "media_urls": {
    "default": ["https://example.com/spring-16x9.jpg"],
    "instagram": ["https://example.com/spring-square.jpg"],
    "pinterest": ["https://example.com/spring-2x3-tall.jpg"],
    "x": []
  }
}
```

- Instagram gets the square crop
- Pinterest gets the tall 2:3 crop for feed visibility
- Facebook falls back to the 16:9 `default`
- X is opted out of media entirely (empty array) so the post goes out as text-only

The empty-array trick is how you say "this platform specifically should not have media" without having to remove it from `accounts`.

## Handling character limits

Every platform has its own character limit. OmniSocials validates against the strictest limit of every channel in the `accounts` array and rejects the whole post with `400` if any platform would exceed it.

| Platform | Limit |
|----------|-------|
| X (standard) | 280 |
| X (Premium) | 25,000 |
| Bluesky | 300 (graphemes) |
| Threads | 500 |
| Mastodon | 500 (default) |
| Pinterest | 500 |
| LinkedIn | 3,000 |
| TikTok | 4,000 |
| Instagram | 2,200 |
| Facebook | 63,206 |
| Google Business | 1,500 |

Two strategies to avoid rejection:

1. **Write to the strictest limit.** If you're posting to X along with LinkedIn and Facebook, cap your copy at 280 characters.
2. **Use per-platform content** to give X a shorter variant and let the others use a longer one.

Strategy 2 is usually the better call because you write once in `default` and only override X.

## Platform-specific options apply automatically

Options that live under a channel-ID key (`pinterest.board_id`, `youtube.title`, `tiktok.privacy_level`, etc.) only apply to that channel. They're ignored by every other channel in the same request.

```json
{
  "content": { "default": "New video drop" },
  "accounts": [
    "tiktok-id",
    "youtube-id",
    "instagram-id"
  ],
  "media_urls": ["https://example.com/vertical-video.mp4"],
  "type": "reel",
  "tiktok": {
    "privacy_level": "PUBLIC_TO_EVERYONE"
  },
  "youtube": {
    "title": "Behind the scenes",
    "privacy_status": "public"
  }
}
```

TikTok picks up the `tiktok` options. YouTube picks up the `youtube` options. Instagram ignores both and uses its own defaults. No per-channel request splitting required.

## What happens when one platform fails

Cross-posting introduces a partial-failure mode: the post publishes to some platforms and fails on others. When that happens, OmniSocials sets the post `status` to `warning` (not `posted` or `failed`). The live URLs of the platforms that succeeded go in `published_urls`, and the per-platform error messages of the ones that failed go in `errors`. Both are objects keyed by platform identifier — succeeded platforms are omitted from `errors`, failed platforms are omitted from `published_urls`.

```json
{
  "id": "123",
  "status": "warning",
  "published_urls": {
    "facebook": "https://www.facebook.com/123456789/posts/987654321",
    "instagram": "https://www.instagram.com/p/Cxyz123ABC/",
    "linkedin": "https://www.linkedin.com/feed/update/urn:li:share:7191234567890"
  },
  "errors": {
    "x": "Duplicate content posted within the last 24 hours"
  }
}
```

(A post where *every* platform fails gets `status: "failed"` with all channels in `errors`; a post where every platform succeeds gets `status: "posted"` and `errors: null`.)

Two ways to handle a partial failure:

1. **Fix and re-publish.** Address the cause first if needed — reconnect the account, or edit the offending copy with `PATCH /posts/:id` (e.g. tweak the X text to dodge the duplicate filter) — then call `POST /posts/:id/publish` to re-run publishing. A fully `failed` post cannot be re-published; create a new post instead.
2. **Listen via webhooks.** A partial failure fires the `post.published` event with `data.status: "warning"`; inspect `data.targets` for the per-platform outcome. See [Webhooks](/webhooks).

## Schedule once, publish everywhere

Cross-posting works the same way for scheduled posts. Add `scheduled_at` (ISO 8601 UTC) and OmniSocials publishes to every channel in the `accounts` array at the scheduled time.

```json
{
  "content": {
    "default": "Friday reading list",
    "x": "This week's top reads 🧵"
  },
  "accounts": ["linkedin-id", "x-id", "threads-id"],
  "scheduled_at": "2026-04-18T14:00:00Z"
}
```

Every channel publishes within a one-minute window of the scheduled time. Scheduling a cross-post does not lock anything in. You can still `PATCH /posts/:id` to change content, `DELETE /posts/:id` to cancel, or add and remove channels before the scheduled time arrives.

## Practical recipes

### A product launch

You want to announce a launch across every channel with platform-appropriate voice.

```json
{
  "content": {
    "default": "It's here. [Feature Name] is live.",
    "x": "Just shipped: [Feature Name] 🚀\n\nLink in replies 👇",
    "linkedin": "We're thrilled to announce the launch of [Feature Name]. For the past six months, our team has been...",
    "linkedin_page": "[Feature Name] is now generally available. Key capabilities include...",
    "threads": "We shipped [Feature Name] today. Open to questions."
  },
  "accounts": [
    "fb-id", "ig-id", "linkedin-id", "linkedin-page-id", "x-id", "threads-id"
  ],
  "media_urls": ["https://example.com/launch-hero.jpg"]
}
```

### A visual campaign

Same photo, different crops, one request.

```json
{
  "content": { "default": "Spring 2026 lookbook" },
  "accounts": ["ig-id", "pinterest-id", "fb-id"],
  "media_urls": {
    "default": ["https://example.com/spring-16x9.jpg"],
    "instagram": [
      "https://example.com/spring-square-1.jpg",
      "https://example.com/spring-square-2.jpg",
      "https://example.com/spring-square-3.jpg"
    ],
    "pinterest": ["https://example.com/spring-tall.jpg"]
  },
  "pinterest": {
    "board_id": "your-board-id",
    "title": "Spring 2026 lookbook"
  }
}
```

Instagram gets a three-image carousel of square crops. Pinterest gets the tall 2:3 crop with board metadata. Facebook falls back to the 16:9 default. One request.

### Schedule a week of daily posts

Cross-posting composes naturally with scheduling. For a week of daily posts, call `POST /posts/create` once per day with a different `scheduled_at`, or batch them in a loop on your side.

```javascript
const posts = [
  { day: "Monday",    copy: "Monday thoughts..." },
  { day: "Tuesday",   copy: "Tuesday tip..." },
  // ...
]

for (const post of posts) {
  await fetch("https://api.omnisocials.com/v1/posts/create", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.OMNISOCIALS_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      content: { default: post.copy },
      accounts: ["linkedin-id", "x-id", "threads-id"],
      scheduled_at: scheduleFor(post.day),
    }),
  })
}
```

Each call creates a separate scheduled post. Each one fans out across the three channels at its scheduled time. Rate limits apply per key (100 req/min) so batching more than 100 posts in a single burst will start hitting 429 responses. Back off and retry, or split across multiple keys.

## Related reading

- [Creating Posts](/creating-posts) for the full field reference
- [Platforms](/platforms) for per-platform options and limits
- [Rate Limits and Errors](/rate-limits-and-errors) for `429` and partial-failure (`warning`) handling
- [Webhooks](/webhooks) for real-time status notifications
