Last updated: September 2026

Is there one API endpoint that posts to X and Bluesky at once?

Yes. POST /api/v1/posts with platforms: ["x","bluesky"] publishes a native post on each network from one request, and returns a result per network so you know what happened on each.

One thing to get right before you ship: omitting the platforms field does not mean X. Below is the parameter, the response shape, and the two calls worth making before your first real post.

The call

One bearer key, one endpoint. Each network is delivered separately through its own API, so what lands on Bluesky is a Bluesky post and not a link back to X.

bash
curl -X POST https://opentweet.io/api/v1/posts \
  -H "Authorization: Bearer ot_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Shipped cross-posting today.",
    "platforms": ["x", "bluesky"],
    "publish_now": true
  }'

Swap publish_now for scheduled_date and the same rules apply at publish time. In a bulk create, platforms can be set per post or once at the top level for the whole batch.

The four cases, including the one that catches people

platforms value
Where it goes
Note
["x", "bluesky"]
Both networks
Explicit values are honoured exactly and never expanded.
["x"]
X only
The way to stay X-only regardless of the account setting.
["bluesky"]
Bluesky only
Useful when the X meter is not worth it for this post.
field omitted
Depends on the account
Follows auto cross-post. This is the one that catches people out.

Omitting platforms is not the same as pinning to X

A post with no platforms field follows the account auto cross-post setting, and when that setting is on it goes to every connected network. If an existing integration has been writing X-only posts by leaving the field out, turning auto cross-post on starts sending those posts to Bluesky as well. Pass an explicit value and this stops being a question.

Read the outcome per network

The response carries a results array, one entry per network, so a partial outcome is visible rather than hidden behind a single flag.

response.json
{
  "results": [
    {
      "platform": "x",
      "status": "published",
      "post_id": "1834000000000000001",
      "url": "https://x.com/you/status/1834000000000000001"
    },
    {
      "platform": "bluesky",
      "status": "skipped",
      "skip_reason": "Text is 412 characters. Bluesky allows 300."
    }
  ]
}

A post that does not fit one network is skipped there with the reason recorded, and still goes out everywhere it fits. That is deliberate: the alternative is failing the whole call, or truncating your words without asking.

Check first, then post

GET /api/v1/cross-posting returns the auto cross-post setting, the deliverable targets, the connected accounts, and the per-network limits. Call it once at startup so your code knows the rules it is writing against.

Limit
X
Bluesky
Text length
280 characters, or 25,000 on Premium
300 characters
Images per post
4
4, at 2MB each
Alt text
Yes
Yes
Threads
Yes
Yes
Video
Yes
No
Analytics read back
Yes
No

Worth being clear about what this is not. OpenTweet is X-native and goes deep on X, with analytics, evergreen recycling, DM campaigns and Articles. Bluesky is a mirror for posts you are already writing, which is why the Bluesky column above has honest gaps in it. LinkedIn and Threads are on the roadmap and are not shipped. Full parameter reference in the cross-posting docs.

7-day free trial. Cancel anytime.

Frequently asked questions

Is there one API endpoint that posts to X and Bluesky at once?

Yes. POST https://opentweet.io/api/v1/posts takes a platforms array, and platforms: ["x","bluesky"] publishes a native post on each network from one request. It is not a link from one to the other, and it is not a repost. Each network gets its own real post through its own API.

What happens if I leave the platforms field out?

It does not mean X. A post with no platforms field follows the account auto cross-post setting, and when that setting is on it goes to every connected network. An explicit platforms value is always honoured exactly and never expanded, so automation that must stay X-only should pass ["x"] rather than omitting the field.

What happens if the post fits one network and not the other?

It publishes where it fits and is skipped where it does not, with the reason recorded against the post. Bluesky allows 300 characters and X allows 280, or 25,000 on Premium, so a 400-character post publishes on a Premium X account and comes back as skipped on Bluesky with a stated reason. Nothing is silently truncated and the whole call does not fail.

How do I know where a post will actually land before I send it?

Call GET /api/v1/cross-posting. It returns the auto cross-post setting, the deliverable targets, which accounts are connected on each network, and the per-network limits including the character count and image rules. Read that once at startup rather than guessing.

Does the response tell me what happened on each network?

Yes. The post carries a results array with one entry per network, each with a status of published, failed or skipped, plus the platform post id and url on success and a skip_reason when it was skipped. You do not have to infer the outcome from a single overall flag.

Do I need an X developer account or a Bluesky API key for this?

Neither. OpenTweet holds the X connection for you, so there is no developer app to register and no pay-per-use meter, and Bluesky has no API keys to issue in the first place. One ot_ bearer key covers both, on a flat $11.99 a month.

One POST, both networks

No X developer account, no Bluesky API key, no per-post fee. Flat $11.99 a month.

7-day free trial. Cancel anytime.