X (Twitter) Publishing
Learn how to connect X (Twitter) accounts and post tweets with text, images, and videos.
⚠️ Ultra plan only. Connecting an X account is available on every plan, but publishing to X requires the Ultra plan (priced at 2× Pro). Ultra allows up to 60 X posts per month.
Connecting X Account
Prerequisites
- An X (Twitter) account
- Signed in to Boring Dashboard
Connection Steps
- Click the "Connect X (Twitter)" button on the dashboard
- Authorize the app on X (Twitter)
- Review and grant the required permissions:
tweet.read- Read tweetstweet.write- Post and delete tweetsusers.read- Read your profile informationmedia.write- Upload images and videosoffline.access- Maintain long-term access
- Click "Authorize app" to complete the connection
Your X account will now appear in the Authorized Accounts list with:
- Username (@username)
- Profile picture
- Platform badge (X)
- Unique Account ID (click "Copy ID" to get it)
- Disconnect option
Token Information
- Token Type: OAuth 2.0 with PKCE (Proof Key for Code Exchange)
- Access Token: Expires in 2 hours
- Refresh Token: Expires in 6 months
- Auto-refresh: Enabled (tokens automatically refreshed before publishing)
- Re-authorization: Only needed after 6 months or if manually revoked
Supported Content Types
X (Twitter) supports tweets with various media types:
| Feature | Description | Required | Limits |
|---|---|---|---|
| Text | Tweet text | No* | Max 280 characters |
| Images | Photo attachments | No | Max 4 images, 5MB each |
| Video | Video attachment | No | Max 1 video, 512MB |
*At least text or media is required
Publishing Examples
1. Text-Only Tweet
{
"post": {
"accountId": "your-x-account-id",
"content": {
"text": "Hello from Boring API! 🚀 This is a text-only tweet. #API #Automation",
"mediaUrls": [],
"platform": "x"
},
"target": {
"targetType": "x"
}
}
}
Character Limit: Text is automatically truncated to 280 characters if longer.
2. Tweet with Images (1-4 images)
{
"post": {
"accountId": "your-x-account-id",
"content": {
"text": "Check out these amazing photos! 📸\n\n#Photography #Travel",
"mediaUrls": [
"https://storage.example.com/photo1.jpg",
"https://storage.example.com/photo2.jpg",
"https://storage.example.com/photo3.jpg"
],
"platform": "x"
},
"target": {
"targetType": "x"
}
}
}
Image Requirements:
- Format: JPG, PNG, GIF, WEBP
- Max size: 5MB per image
- Max count: 4 images per tweet
- Resolution: Up to 8192x8192 pixels
- URL: Must be publicly accessible
3. Tweet with Video
{
"post": {
"accountId": "your-x-account-id",
"content": {
"text": "New video tutorial! 🎥 Learn how to use our API in 5 minutes.\n\n#Tutorial #API #DevTools",
"mediaUrls": ["https://storage.example.com/tutorial.mp4"],
"platform": "x"
},
"target": {
"targetType": "x"
}
}
}
Video Requirements:
- Format: MP4, MOV, AVI
- Max size: 512MB
- Max duration: 2 minutes 20 seconds (140 seconds)
- Resolution: 1920x1200 max, 32x32 min
- Frame rate: 40 fps max
- Aspect ratio: 1:2.39 to 2.39:1
- URL: Must be publicly accessible
Note: Only one video per tweet. You cannot mix images and videos.
API Request Format
Full Example with Python
import requests
API_URL = "https://boring.aiagent-me.com/v2/posts"
API_KEY = "boring_xxxxxxxxxxxxx"
ACCOUNT_ID = "your-x-account-id"
# Tweet with images
post_data = {
"post": {
"accountId": ACCOUNT_ID,
"content": {
"text": "Exciting news! 🎉 We just launched our new feature. Check it out!\n\n#ProductLaunch #Innovation",
"mediaUrls": [
"https://storage.googleapis.com/my-bucket/feature-screenshot.jpg"
],
"platform": "x"
},
"target": {
"targetType": "x"
}
}
}
headers = {
"boring-api-key": API_KEY,
"Content-Type": "application/json"
}
response = requests.post(API_URL, headers=headers, json=post_data)
result = response.json()
if result.get("success"):
print(f"Tweet posted successfully!")
print(f"Tweet ID: {result['tweet_id']}")
print(f"Tweet URL: {result['tweet_url']}")
else:
print(f"Tweet failed: {result.get('error')}")
Success Response
{
"success": true,
"message": "Post published successfully",
"postSubmissionId": "uuid-here",
"platform": "x",
"post_type": "photo",
"tweet_id": "1234567890123456789",
"tweet_url": "https://x.com/i/status/1234567890123456789",
"media_count": 1
}
Media Upload Process
Media and tweets are published with each connected account's own OAuth 2.0 token:
- Download Media: Media files are downloaded from provided URLs
- OAuth 2.0 Upload: Media is uploaded to X using the connected account's OAuth 2.0 token (v2
/2/media/upload, requires themedia.writescope), so the media is owned by the posting account - Processing: X processes the media (especially for videos)
- OAuth 2.0 Tweet: Tweet is created with the uploaded media (v2
/2/tweets)
Video Processing
Videos require additional processing time:
[X] Uploading video in chunks...
[X] Upload progress: 33%
[X] Upload progress: 67%
[X] Upload progress: 100%
[X] Video processing status: pending
[X] Video processing status: processing
[X] Video processing status: succeeded
[X] Tweet posted successfully!
Typical processing times:
- Small videos (<10MB): 5-10 seconds
- Medium videos (10-50MB): 10-30 seconds
- Large videos (50-512MB): 30-90 seconds
Character Limit and Text Handling
280 Character Limit
X enforces a strict 280 character limit. Boring automatically handles this:
# Text longer than 280 characters
text = "This is a very long tweet..." * 50 # 1000+ characters
# Boring automatically truncates to 280 characters
post_data = {
"post": {
"accountId": ACCOUNT_ID,
"content": {
"text": text, # Will be truncated
"platform": "x"
},
"target": {"targetType": "x"}
}
}
Result: Text is truncated to first 280 characters.
Unicode and Emojis
Emojis and special characters count differently:
- Regular ASCII characters: 1 character each
- Emojis: Usually 2 characters each
- URLs: Shortened to 23 characters (t.co links)
Example:
"Hello 👋 World 🌍" = 14 characters (2 for each emoji)
"Check out https://example.com/very/long/url" = ~30 characters (URL = 23 chars)
API Request Examples
cURL Example
curl -X POST https://boring.aiagent-me.com/v2/posts \
-H "boring-api-key: boring_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"post": {
"accountId": "your-x-account-id",
"content": {
"text": "Testing the Boring API with X! 🚀 #API #Testing",
"mediaUrls": [],
"platform": "x"
},
"target": {
"targetType": "x"
}
}
}'
JavaScript/Node.js Example
const axios = require('axios');
const API_URL = 'https://boring.aiagent-me.com/v2/posts';
const API_KEY = 'boring_xxxxxxxxxxxxx';
const ACCOUNT_ID = 'your-x-account-id';
async function postTweet(text, mediaUrls = []) {
const postData = {
post: {
accountId: ACCOUNT_ID,
content: {
text: text,
mediaUrls: mediaUrls,
platform: 'x'
},
target: {
targetType: 'x'
}
}
};
try {
const response = await axios.post(API_URL, postData, {
headers: {
'boring-api-key': API_KEY,
'Content-Type': 'application/json'
}
});
console.log('Tweet posted:', response.data);
return response.data;
} catch (error) {
console.error('Error posting tweet:', error.response?.data || error.message);
throw error;
}
}
// Post a text-only tweet
postTweet('Hello from Node.js! 👋 #NodeJS #API');
// Post a tweet with an image
postTweet(
'Check out this beautiful sunset! 🌅 #Photography',
['https://storage.example.com/sunset.jpg']
);
Troubleshooting
Common Errors
Error: "Account is not an X (Twitter) account"
- Cause: Wrong Account ID
- Solution: Use the Account ID from your X account (visible in dashboard after connection)
Error: "Too many media files"
- Cause: More than 4 images provided
- Solution: Limit to maximum 4 images per tweet
Error: "Cannot mix images and videos"
- Cause: Provided both images and video
- Solution: Tweet can have either 1 video or up to 4 images, not both
Error: "Failed to upload media"
- Cause: Media URL not accessible or invalid format
- Solution:
- Verify URL is publicly accessible:
curl -I https://your-media-url.jpg - Check file format (JPG, PNG for images; MP4, MOV for videos)
- Ensure file size within limits (5MB for images, 512MB for videos)
- Verify URL is publicly accessible:
Error: "Video processing failed"
- Cause: X couldn't process the video
- Solution:
- Check video format (MP4 recommended)
- Ensure duration under 140 seconds
- Verify video codec (H.264 recommended)
- Try re-encoding video with standard settings
Error: "Token refresh failed"
- Cause: Refresh token expired (after 6 months) or revoked
- Solution: Disconnect and reconnect your X account
Best Practices
- Keep tweets concise - 280 characters is the limit
- Use hashtags strategically - 2-3 relevant hashtags maximum
- Optimize images - Use high-quality JPG or PNG files
- Compress videos - Keep under 50MB for faster upload
- Test URLs first - Verify media URLs are accessible
- Monitor rate limits - Space out tweets to avoid rate limiting
- Check tweet preview - Preview in dashboard before publishing
Rate Limits
X API has the following rate limits per user:
- Post tweets: 300 tweets per 3 hours
- Media upload: 500 images per 15 minutes
- Video upload: Limited by size (512MB total per 24 hours)
Boring handles rate limit errors gracefully and returns appropriate error messages.
Boring Ultra plan limit: X publishing is capped at 60 posts per month per user (resets at the start of each month, counted across all your connected X accounts). Exceeding it returns X_MONTHLY_LIMIT_EXCEEDED (HTTP 429).
Publishing History
View all your tweets in the dashboard:
- Sign in to Boring Dashboard
- Scroll to "Publish History" section
- Filter by platform: X
Each entry shows:
- Tweet text
- Tweet ID and URL
- Media count
- Timestamp
- Status (published/failed)
- Error message (if failed)
Advanced Features
Multiple X Accounts
You can connect multiple X accounts to the same Boring account:
# Account 1: Personal (@john_personal)
personal_account_id = "account-id-1"
# Account 2: Business (@john_business)
business_account_id = "account-id-2"
# Post to personal account
post_to_account(personal_account_id, "Personal tweet! 👋")
# Post to business account
post_to_account(business_account_id, "Business announcement! 📢")
Batch Posting
Post multiple tweets programmatically:
tweets = [
{"text": "Tweet 1: Introduction 👋", "media": []},
{"text": "Tweet 2: Key features 🚀", "media": ["feature.jpg"]},
{"text": "Tweet 3: Final thoughts 💭", "media": []}
]
for tweet in tweets:
post_data = {
"post": {
"accountId": ACCOUNT_ID,
"content": {
"text": tweet["text"],
"mediaUrls": tweet["media"],
"platform": "x"
},
"target": {"targetType": "x"}
}
}
response = requests.post(API_URL, headers=headers, json=post_data)
print(f"Posted: {tweet['text']} - {response.json()}")
# Wait between tweets to avoid rate limits
time.sleep(60) # 60 seconds delay
Security Notes
- OAuth 2.0 with PKCE: Enhanced security for authorization flow
- Per-account tokens: Media and tweets both use the connected account's OAuth 2.0 token (X API v2)
- Token Storage: Tokens securely stored in MongoDB with encryption
- Auto-refresh: Tokens automatically refreshed before expiration
- Revocation: You can disconnect your account anytime from the dashboard
Quick Reference
Minimal Tweet (Text Only)
{
"post": {
"accountId": "your-account-id",
"content": {
"text": "Hello World! 🌍",
"platform": "x"
},
"target": {"targetType": "x"}
}
}
Tweet with Image
{
"post": {
"accountId": "your-account-id",
"content": {
"text": "Check this out! 📸",
"mediaUrls": ["https://example.com/image.jpg"],
"platform": "x"
},
"target": {"targetType": "x"}
}
}
Tweet with Video
{
"post": {
"accountId": "your-account-id",
"content": {
"text": "Watch this! 🎥",
"mediaUrls": ["https://example.com/video.mp4"],
"platform": "x"
},
"target": {"targetType": "x"}
}
}
Next Steps
- Facebook Publishing - Learn about Facebook
- Instagram Publishing - Learn about Instagram
- Threads Publishing - Learn about Threads
- YouTube Publishing - Learn about YouTube
- API Reference - Complete API documentation
- Examples - More code examples