# X

Publish posts, threads, replies, quotes and polls to X with the Zernio API, then read analytics, retweet, bookmark, follow and answer DMs from the same account.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { PlatformCapabilities } from '@/components/platform-capabilities';

Publish text, images, GIFs, videos, threads, replies, quotes and polls to X (platform value `twitter`) with `POST /v1/posts` and `platform: "twitter"`. The same account also serves analytics, retweets, bookmarks, follows, DMs and comments.

## Quick reference

<PlatformCapabilities rows={[
  { property: 'Character limit', value: '280 (free) / 25,000 (Premium)' },
  { property: 'Images per post', value: '4 (or 1 GIF)' },
  { property: 'Videos per post', value: '1' },
  { property: 'File limits', value: <>Formats, sizes and duration (see <a href="/platforms/twitter/media">Media &amp; Video</a>)</> },
  { property: 'Threads', value: 'Yes (threadItems)' },
  { property: 'Scheduling', value: 'Yes' },
  { property: 'Inbox (DMs)', value: 'Yes' },
  { property: 'Inbox (Comments)', value: 'Yes' },
  { property: 'Analytics', value: 'Yes' },
]} />

## Before you start

X requires a card on file. X bills every API call at its published price, and Zernio passes that cost through with no markup.

Until the team has a payment method, `GET /v1/connect/twitter` returns `402` with `reason: "twitter_passthrough"`. The rates and the monthly spend cap are on [X API usage](/pricing#x-twitter-api-usage).

A free X account is limited to 280 characters, a Premium account to 25,000.

URLs count as 23 characters whatever their length, and emojis count as 2. X also rejects a post whose text matches one it already has.

<Callout type="warn">
**Cross-posting** from a platform with a higher limit (LinkedIn 3,000, Facebook 63,206)? Set `customContent` on the X entry with a version under 280 characters.

Otherwise the X copy fails at publish time with "Tweet text is too long".
</Callout>

## Connect

Call `GET /v1/connect/twitter` with `profileId` ([Get OAuth connect URL](/connect/get-connect-url)). Send the user's browser to the returned `authUrl`.

The [connecting accounts guide](/guides/connecting-accounts) covers the flow and [scopes](/guides/connecting-accounts#scopes) in general.

[Account health](/accounts/get-all-accounts-health) reports what a connected account can do with the scopes the user granted.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: connect } = await zernio.connect.getConnectUrl({
  path: { platform: 'twitter' },
  query: { profileId: '66a1f0c2a4b9d3e8f1a2b3c4', redirect_url: 'https://myapp.com/callback' }
});
// Send the user's browser to connect.authUrl
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

connect = client.connect.get_connect_url(
    platform="twitter",
    profile_id="66a1f0c2a4b9d3e8f1a2b3c4",
    redirect_url="https://myapp.com/callback",
)
# Send the user's browser to connect["authUrl"]
```
</Tab>
<Tab value="curl">
```bash
curl "https://zernio.com/api/v1/connect/twitter?profileId=66a1f0c2a4b9d3e8f1a2b3c4&redirect_url=https://myapp.com/callback" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```
</Tab>
</Tabs>

Response (`200`):

```json
{
  "authUrl": "https://twitter.com/i/oauth2/authorize?client_id=...",
  "state": "..."
}
```

After approval the user lands on `redirect_url` with `connected=twitter&profileId=...&accountId=...` appended. That `accountId` is the value every sample in this section uses.

## Publish

Call `POST /v1/posts` with `content`, `publishNow: true` and a `platforms` entry. The entry carries `platform: "twitter"` and the `accountId` from the connect flow.

Replace `publishNow` with `scheduledFor` and `timezone` to schedule instead.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: published } = await zernio.posts.createPost({
  body: {
    content: 'Shipping day. The API is live.',
    platforms: [
      { platform: 'twitter', accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
    ],
    publishNow: true
  }
});

console.log(published.post.platforms[0].platformPostUrl);
```
</Tab>
<Tab value="Python">
```python
published = client.posts.create_post(
    content="Shipping day. The API is live.",
    platforms=[
        {"platform": "twitter", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
    ],
    publish_now=True
)

print(published["post"]["platforms"][0]["platformPostUrl"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST https://zernio.com/api/v1/posts \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Shipping day. The API is live.",
    "platforms": [
      {"platform": "twitter", "accountId": "66b2e19d8c3f5a7e9d0b1c2d"}
    ],
    "publishNow": true
  }'
```
</Tab>
</Tabs>

Response (`201`):

```json
{
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "published",
    "platforms": [
      {
        "platform": "twitter",
        "status": "published",
        "platformPostId": "1852634789012345678",
        "platformPostUrl": "https://twitter.com/acmecorp/status/1852634789012345678"
      }
    ]
  }
}
```

Media, threads, replies, quotes and polls all change this one request. [Posts & Editing](/platforms/twitter/posts) has each of them.

## Common errors

| Error | Cause | Fix |
|-------|-------|-----|
| `402` with `reason: "twitter_passthrough"` on `GET /v1/connect/twitter` | The team has no card on file, and X charges for every API call. | Send the user to the `dashboard_url` in the response, then call connect again. Rates are on [X API usage](/pricing#x-twitter-api-usage). |
| `?error=oauth_denied&platform=twitter` appended to your `redirect_url` | The user denied consent, or X rejected the callback. | Start the flow again. Treat an unknown `error` value as a generic failure; new values are added without notice. |
| `401` on a later call for that account | X revoked or expired the token. | Reconnect the account. [Account health](/accounts/get-all-accounts-health) reports it before a post fails. |

The publish-time errors are in [Limits & Errors](/platforms/twitter/reference#common-errors), and [Error handling](/guides/error-handling) covers the envelope.

## In this section

<PlatformPages platform="twitter" />

## Related

- [Connecting accounts](/guides/connecting-accounts): the OAuth flow and the `402` billing gates.
- [Create post](/posts/create-post): every field of the request.
- [Media uploads](/guides/media-uploads): upload images and videos instead of hosting them.
- [Twitter engagement](/twitter-engagement/retweet-post): retweet, bookmark, follow, search and look up posts.
- [X API usage](/pricing#x-twitter-api-usage): pass-through rates and the spend cap.