# X (Twitter) API: Posting Reference

X (formerly Twitter) is supported via the X API v2. OmniSocials posts to the account that completed the OAuth flow. Both standard and premium accounts are supported, and the character limit adjusts based on the account's subscription tier.

**Channel ID:** `x`

## Supported content types

| Type | Supported |
|------|-----------|
| Feed post | ✅ |
| Story | - |
| Reel | - |

## Minimal example

```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": "Hello, X 👋" },
    "accounts": ["your-x-account-id"]
  }'
```

## Platform-specific options

| Field | Type | Description |
|-------|------|-------------|
| `x.reply_settings` | string | Who can reply: `""` (everyone, default), `following`, or `mentionedUsers` |
| `x.paid_partnership` | boolean | Mark as a paid partnership disclosure (default `false`) |
| `x.made_with_ai` | boolean | Mark as AI-generated content (default `false`) |
| `x.thread_parts` | array | Publish as a chained thread instead of a single tweet. See [Posting threads](#posting-threads). |

```json
{
  "content": { "default": "Limited reply post" },
  "accounts": ["your-x-account-id"],
  "x": {
    "reply_settings": "following"
  }
}
```

## Posting threads

To publish as a thread, pass `x.thread_parts` instead of relying on `content.x`. Each part becomes its own tweet, chained via `in_reply_to_tweet_id`.

```json
{
  "accounts": ["your-x-account-id"],
  "content": { "default": "" },
  "x": {
    "thread_parts": [
      { "text": "1/3: kicking off a thread on building scalable APIs" },
      { "text": "2/3: keep the boundary small, document at the edges" },
      { "text": "3/3: and ship sooner than you think" }
    ]
  }
}
```

**Rules:**

- 2 to 25 parts. For a single tweet, omit `thread_parts` and use `content.x` (or `content.default`) instead.
- Each `text` is required, non-empty, and ≤ 280 characters. Parts that exceed the limit are rejected with a `400`.
- `media_urls` is optional per part (max 4) and overrides any top-level `media_urls.x` for that specific tweet:

  ```json
  {
    "x": {
      "thread_parts": [
        {
          "text": "Photo dump from the trip 📷",
          "media_urls": ["https://example.com/1.jpg", "https://example.com/2.jpg"]
        },
        { "text": "More to come tomorrow." }
      ]
    }
  }
  ```

- When `content.x` is provided alongside `thread_parts`, `thread_parts` wins.
- The first tweet's URL becomes `x_posted_url` on the post. The full chain is recorded on `x_values.thread_tweets` for analytics.

To convert a thread back into a single tweet on update, send `x.thread_parts: null` to `PATCH /posts/:id`.

## Character limits

| Account tier | Character limit |
|--------------|-----------------|
| Standard | 280 |
| Premium (Blue) | 25,000 |
| Premium+ | 25,000 |

For single tweets, OmniSocials detects the connected account's subscription tier and enforces the correct limit at validation time. Thread parts always cap at 280 chars regardless of tier; that's an X API constraint, not an OmniSocials one.

**Characters are weighted the way X counts them, not by string length.** Every link counts as 23 characters no matter how long the URL is, and emoji and CJK characters count as 2 each (an emoji family joined with ZWJ counts as a single 2-weight unit). A 270-character post containing a 10-character link is over the limit. OmniSocials validates with the weighted count, so a rejection at create time means X would have rejected it too; X itself returns an unhelpful generic `403` for over-length posts.

## Media requirements

| Media | Requirement |
|-------|-------------|
| Image | JPEG, PNG, GIF, or WebP. Max 5 MB. Up to 4 per post. |
| Video | MP4, H.264 codec. Free and Basic accounts: up to 2 minutes 20 seconds, max 512 MB. Premium and Premium+ accounts: up to 125 minutes, max 16 GB (in practice bounded by OmniSocials' own limits: 1 GB for media added by URL, 100 MB for direct file uploads). The tier is detected from your connected X account; reconnect X after upgrading. |

X does not support mixing image and video in the same post. Video posts allow one video. Image posts allow up to four images.

**Alt text.** Pass a per-image accessibility description by using an object entry instead of a bare string: `{ "url": "https://example.com/chart.png", "alt": "Bar chart of Q3 signups by week" }` in `media_urls` (or `{ "id": "...", "alt": "..." }` in `media_ids`, including per-part thread media). It is set via X's v2 media metadata on photos and GIFs only (X does not support alt text on video), clamped to X's 1000-character cap. An alt-text failure at X never fails the publish.

## Link posts use credits

X's API bills posts whose text contains a URL at a premium ($0.20 per tweet instead of $0.015). OmniSocials passes that fee through as prepaid credits at X's rate card: **20 credits per URL-containing tweet** (credits cost €0.01 each, topped up €5-500 in the dashboard), with threads billed per part that contains a link. Everything else on X stays free: posts without links, analytics, media.

URL detection matches X's own autolinker: scheme links (`https://...`), `www.` links, and bare domains with common TLDs all count; spelled-out domains ("brand dot com") don't.

When a create targets X and the text contains a URL, the `201` response includes a top-level `warnings` array (code `x_url_post_credits`) with `credits_required`, the current `credits_balance`, and the enforcement state; see [Creating Posts](/creating-posts#x-link-posts-use-credits) for the full shape. Debiting has been live since **2026-08-14**. If the balance can't cover it at publish time, only the X target fails (other platforms publish normally); top up at **[app.omnisocials.com/credits](https://app.omnisocials.com/credits)** and retry with `POST /posts/:id/retry`. Credits are managed in the dashboard; there is no API endpoint for them.

Scheduled X link posts also *reserve* their cost until they publish: scheduling or publishing a new one that would push the reserved total past the balance is refused with `402 x_credits_insufficient` (see [Creating Posts](/creating-posts#x-link-posts-use-credits)). Drafts are never gated.

## Limitations

- Polls are not supported through the X API v2 for third-party apps
- Posting to X requires approval by X for the OAuth app. Newly created X developer accounts may be subject to rate limits lower than the platform default.
