---
slug: "opentweet-x-poster-x-5"
source_type: "clawhub"
source_url: "https://clawhub.ai/skills/opentweet-x-poster"
repo: ""
source_file: "description"
---
---
name: x-poster
description: Post to X (Twitter) using the OpenTweet API. Create tweets, schedule posts, publish threads, write and schedule long-form X Articles from markdown, upload media, run an evergreen queue, search inspiration tweets, repurpose them with AI, run AI-qualified DM outreach campaigns with human-approved sending, and read engagement-weighted analytics, all autonomously.
version: 1.6.0
homepage: https://opentweet.io/features/openclaw-twitter-posting
user-invocable: true
metadata: {"openclaw":{"requires":{"env":["OPENTWEET_API_KEY"]},"primaryEnv":"OPENTWEET_API_KEY"}}
---

# OpenTweet X Poster

You can post to X (Twitter) using the OpenTweet REST API. All requests go to `https://opentweet.io` with the user's API key.

## Authentication

Every request needs this header:
```
Authorization: Bearer $OPENTWEET_API_KEY
Content-Type: application/json
```

For file uploads, use `Content-Type: multipart/form-data` instead.

## Before You Start

ALWAYS verify the connection first:
```
GET https://opentweet.io/api/v1/me
```
Returns subscription status, daily post limits, post counts, and connected X accounts. Check `subscription.has_access` is true and `limits.remaining_posts_today` > 0 before scheduling or publishing.

## Multi-Account Support

Pro users get 1 X account, Advanced 3, Agency 10. Use the `x_account_id` parameter to target a specific account.

### List connected accounts
```
GET https://opentweet.io/api/v1/accounts
```
Returns: `{ "accounts": [{ "id": "...", "x_handle": "@handle", "x_name": "Display Name", "is_primary": true, "nickname": null }] }`

### Using x_account_id
Add `x_account_id` to any POST/PUT body or GET query parameter to target a specific X account:
- **Creating posts**: `{ "text": "...", "x_account_id": "account_id_here" }`
- **Listing posts**: `GET /api/v1/posts?x_account_id=account_id_here`
- **Batch schedule**: `{ "schedules": [...], "x_account_id": "account_id_here" }`
- **Analytics**: `GET /api/v1/analytics/overview?x_account_id=account_id_here`
- **Evergreen**: `GET /api/v1/evergreen/posts?x_account_id=account_id_here`
- **Best-times analyze**: `POST /api/v1/analytics/best-times/analyze` body `{ "x_account_id": "..." }`

When `x_account_id` is omitted, the primary account is used. Single-account users never need to specify it.

## Post Management

### Create a tweet
```
POST https://opentweet.io/api/v1/posts
Body: { "text": "Your tweet text" }
```
Optionally add `"scheduled_date": "2026-05-01T10:00:00Z"` to schedule it (requires active subscription, date must be in the future).

### Create and publish immediately (one step)
```
POST https://opentweet.io/api/v1/posts
Body: { "text": "Hello from the API!", "publish_now": true }
```
Creates the post AND publishes to X in one request. Cannot combine with `scheduled_date` or bulk posts. Response includes `status: "posted"`, `x_post_id`, and `url` (the real X post URL) on success.

### Create a tweet with media
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "Check out this screenshot!",
  "media_urls": ["https://url-from-upload-endpoint"]
}
```
Upload media first via `POST /api/v1/upload`, then pass the returned URL(s) in `media_urls`.

### Create a thread
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "First tweet of the thread",
  "is_thread": true,
  "thread_tweets": ["Second tweet", "Third tweet"]
}
```

### Create a thread with per-tweet media
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "Thread intro with image",
  "is_thread": true,
  "thread_tweets": ["Second tweet", "Third tweet"],
  "media_urls": ["https://intro-image-url"],
  "thread_media": [["https://img-for-tweet-2"], []]
}
```
`thread_media` is an array of arrays. Each inner array contains media URLs for the corresponding tweet in `thread_tweets`. Use `[]` for tweets with no media.

### Post to an X Community
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "Shared with the community!",
  "community_id": "1234567890",
  "share_with_followers": true
}
```

### Auto-retweet a post
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "This will get a boost.",
  "scheduled_date": "2026-05-01T10:00:00Z",
  "auto_retweet_enabled": true,
  "auto_retweet_offset_minutes": 240
}
```
After the post publishes, OpenTweet automatically retweets it from the same account `auto_retweet_offset_minutes` later. Works on PUT too. Range: 1–10080 minutes (up to 7 days). Both fields can also be set via `PUT /api/v1/posts/{id}`.

### Auto-plug a post (reply when it goes viral)
```
POST https://opentweet.io/api/v1/posts
Body: {
  "text": "Hot take about AI agents.",
  "scheduled_date": "2026-05-01T10:00:00Z",
  "auto_plug_enabled": true,
  "auto_plug_threshold": 50,
  "auto_plug_text": "Enjoyed this? I share more every week → link.com/newsletter"
}
```
After the post publishes, OpenTweet checks its like count every 5 minutes. When `like_count >= auto_plug_threshold`, it automatically posts `auto_plug_text` as a reply to the original tweet — turning viral reach into subscribers or leads.

- `auto_plug_threshold` — likes needed to trigger (default: 20, no upper limit)
- `auto_plug_text` — the reply content, max 280 chars (required when `auto_plug_enabled: true`)
- Fires exactly once per post; `auto_plug_done: true` and `auto_plug_tweet_id` are set after sending
- Only checks posts published within the last 30 days
- Works on `PUT /api/v1/posts/{id}` too (set before or after publishing)

### Bulk create (up to 50 posts)
```
POST https://opentweet.io/api/v1/posts
Body: {
  "posts": [
    { "text": "Tweet 1", "scheduled_date": "2026-05-01T10:00:00Z" },
    { "text": "Tweet 2", "scheduled_date": "2026-05-01T14:00:00Z" }
  ]
}
```

### Schedule a post
```
POST https://opentweet.io/api/v1/posts/{id}/schedule
Body: { "scheduled_date": "2026-05-01T10:00:00Z" }
```
The date must be in the future. Use ISO 8601 format.

### Publish immediately
```
POST https://opentweet.io/api/v1/posts/{id}/publish
```
No body needed. Posts to X right now. Response includes `status: "posted"`, `x_post_id`, and `url` (the real X post URL).

### Batch schedule (up to 50 posts)
```
POST https://opentweet.io/api/v1/posts/batch-schedule
Body: {
  "schedules": [
    { "post_id": "id1", "scheduled_date": "2026-05-02T09:00:00Z" },
    { "post_id": "id2", "scheduled_date": "2026-05-03T14:00:00Z" }
  ],
  "community_id": "optional-community-id",
  "share_with_followers": true,
  "x_account_id": "optional-account-id"
}
```

### List posts
```
GET https://opentweet.io/api/v1/posts?status=scheduled&page=1&limit=20
```
Status options: `scheduled`, `posted`, `draft`, `failed`, `evergreen` (returns evergreen pool source posts).

### Get a post
```
GET https://opentweet.io/api/v1/posts/{id}
```

### Update a post
```
PUT https://opentweet.io/api/v1/posts/{id}
Body: {
  "text": "Updated text",
  "media_urls": ["https://..."],
  "scheduled_date": "2026-05-01T10:00:00Z",
  "auto_retweet_enabled": true,
  "auto_retweet_offset_minutes": 120
}
```
All fields optional. Cannot update already-published posts. Set `scheduled_date` to `null` to unschedule (convert back to draft).

### Delete a post
```
DELETE https://opentweet.io/api/v1/posts/{id}
```
Default: if the post was already published, OpenTweet also deletes it from X. To delete only locally and leave the X post live, append `?delete_from_x=false`. Response includes `x_deleted` and (if it failed) `x_delete_error`.

## Media Upload

### Upload an image or video
```
POST https://opentweet.io/api/v1/upload
Content-Type: multipart/form-data
Body: file=@your-image.png
```
Returns: `{ "url": "https://..." }`

Supported formats: JPG, PNG, GIF, WebP (max 5MB), MP4, MOV (max 20MB).

**Workflow**: Upload first, then use the returned URL in `media_urls` or `thread_media` when creating/updating posts.

## AI Media Generation

Generate images and videos with Grok Imagine (xAI) directly from a prompt. The generated file is permanently stored and the URL can be used in `media_urls` when creating a tweet. Requires `XAI_API_KEY` to be configured on the server.

### Generate an image
```
POST https://opentweet.io/api/v1/generate/image
Body: {
  "prompt": "A vibrant product launch announcement graphic",
  "aspect_ratio": "16:9",
  "resolution": "1k"
}
```
- `prompt` — required, max 1000 chars
- `aspect_ratio` — optional: `"1:1"` (default), `"16:9"`, `"9:16"`, `"4:3"`, `"3:4"`
- `resolution` — optional: `"1k"` (default) or `"2k"`

Returns: `{ "url": "https://...", "prompt": "...", "aspect_ratio": "16:9", "resolution": "1k" }`

Response is **synchronous** — the URL is ready to use immediately.

### Generate a video
```
POST https://opentweet.io/api/v1/generate/video
Body: {
  "prompt": "A product spinning on a pedestal with dramatic lighting",
  "aspect_ratio": "16:9",
  "resolution": "480p",
  "duration": 5
}
```
- `prompt` — required, max 1000 chars
- `aspect_ratio` — optional: `"16:9"` (default), `"9:16"`, `"1:1"`, `"4:3"`, `"3:4"`
- `resolution` — optional: `"480p"` (default) or `"720p"`
- `duration` — optional: `5` (default) to `10` seconds

Returns (202): `{ "job_id": "...", "status": "processing", "message": "..." }`

Video generation is **asynchronous** — poll the status endpoint until complete.

### Check video generation status
```
GET https://opentweet.io/api/v1/generate/video/{job_id}
```
Returns one of:
- `{ "job_id": "...", "status": "processing" }` — still generating, poll again in 10 seconds
- `{ "job_id": "...", "status": "completed", "url": "https://..." }` — ready, use the URL
- `{ "job_id": "...", "status": "failed", "error": "..." }` — generation failed

Typical generation time: 1–3 minutes. Poll every 10 seconds.

## X Articles (Long-Form Posts)

Write and publish long-form X Articles from markdown. OpenTweet converts markdown to X's rich-text article format at publish time: `#`/`##`/`###` headings, **bold**, *italic*, ~~strikethrough~~, links, bullet/numbered lists, `>` quotes, inline images via `![caption](url)` (uploaded to X automatically), a cover image, and embedded posts (an x.com status URL on its own line). Fenced code blocks are preserved as plain text.

Requires an active paid subscription (not trial). The connected X account must have X Premium (any tier since January 2026) to publish Articles — if not, publishing fails and `failed_reason` carries X's exact error.

Article statuses: `draft`, `scheduled`, `publishing`, `published`, `failed`.

### Create an article (draft, scheduled, or publish now)
```
POST https://opentweet.io/api/v1/articles
Body: {
  "title": "How We Grew to 10k Users",
  "content_markdown": "# The short version\n\nWe shipped **every week**...",
  "cover_image_url": "https://url-from-upload-endpoint",
  "scheduled_date": "2026-07-15T14:00:00Z"
}
```
- `title` and `content_markdown` are required.
- `scheduled_date` schedules it; `"publish_now": true` publishes immediately (mutually exclusive with `scheduled_date`).
- Omit both to save a draft.
- Optional `x_account_id` for multi-account users.

Returns 201 with the article object. A published article includes `article_url` (the live X link) and `x_post_id`.

### List articles
```
GET https://opentweet.io/api/v1/articles?status=scheduled&page=1&limit=20
```
Status filter options: `draft`, `scheduled`, `publishing`, `published`, `failed`.

### Get / update / delete an article
```
GET    https://opentweet.io/api/v1/articles/{id}
PUT    https://opentweet.io/api/v1/articles/{id}    # body: any of title, content_markdown, cover_image_url, scheduled_date, x_account_id
DELETE https://opentweet.io/api/v1/articles/{id}
```
Published articles cannot be edited. Set `scheduled_date` to `null` to unschedule back to draft. Updating a failed article resets it to draft/scheduled.

### Publish an article immediately
```
POST https://opentweet.io/api/v1/articles/{id}/publish
```
No body needed. Returns the article with `status: "published"` and `article_url`, or 502 with `code: "publish_failed"` and X's error in `failed_reason`.

## DM Campaigns (AI Lead Outreach)

Run outreach campaigns that find qualified X profiles, qualify them with AI, draft a personalized DM for each, and send only after a human approves. This is not a mass messenger: sending is human-in-the-loop, drip-scheduled, rate-limited, and honors opt-outs. Available on the Advanced and Agency plans only (403 on Pro). Requires a paid API key (not the free taste key).

How a campaign flows: you create it with an ideal-customer description and one or more lead sources, discovery finds and AI-qualifies candidates into leads, you review and approve the drafted messages, then the sender drips approved DMs from the connected X account within the campaign's caps. Replies are tracked and tagged, and opt-outs are auto-suppressed. Actual sending is gated server-side and paced, so approving a lead queues it, it does not fire instantly.

### Create a campaign
```
POST https://opentweet.io/api/v1/dm-campaigns
Body: {
  "name": "Shopify founders Q3",
  "x_account_id": "account_id_here",
  "sources": [
    { "type": "search", "value": "shopify analytics", "max_candidates": 500 },
    { "type": "followers_of", "value": "somecompetitor", "max_candidates": 500 },
    { "type": "tweet_engagers", "value": "https://x.com/user/status/123", "max_candidates": 300 },
    { "type": "own_followers", "value": "", "max_candidates": 500 }
  ],
  "icp_prompt": "Shopify store owners or ecommerce founders, 200-20k followers, English, active in the last 2 weeks, complaining about ad costs or analytics. Not agencies.",
  "score_threshold": 60,
  "message_brief": { "offer": "a $29/mo analytics tool for Shopify", "tone": "casual", "cta": "worth a look?", "follow_up_enabled": false },
  "limits": { "dms_per_day": 25, "active_hours_start": 9, "active_hours_end": 18, "timezone": "America/New_York" },
  "accept_policy": true
}
```
`accept_policy` must be `true` (it records acceptance of X's acceptable-use terms; the connected account bears the outreach risk). Returns `{ "campaign": {...} }`. Source types: `search` (keyword query), `followers_of` (a handle), `tweet_engagers` (a tweet URL), `own_followers`.

### List campaigns
```
GET https://opentweet.io/api/v1/dm-campaigns
```
Returns `{ "campaigns": [...] }`, each with a `stats` funnel (found, qualified, approved, sent, replied).

### Get a campaign (with stats)
```
GET https://opentweet.io/api/v1/dm-campaigns/{id}
```

### Start discovery
```
POST https://opentweet.io/api/v1/dm-campaigns/{id}/discover
```
Sets the campaign to `discovering` and returns immediately. Discovery and AI qualification run in the background, then the campaign moves to `reviewing` with leads ready. Poll the campaign until its status is `reviewing`.

### List leads (the review queue)
```
GET https://opentweet.io/api/v1/dm-campaigns/{id}/leads?state=qualified&page=1
```
State filter includes `qualified` (awaiting approval), `approved`, `sent`, `replied`, `skipped`. Each lead has a `score`, a `reason`, an `angle_hook`, and a drafted `final_message`.

### Approve or skip a lead
```
PATCH https://opentweet.io/api/v1/dm-campaigns/{id}/leads/{leadId}
Body: { "action": "approve", "final_message": "optional edited message" }
```
`action` is `approve` (queues the drafted or provided message for drip sending), `skip`, or `edit` (change the draft without approving). Approving does NOT send immediately; the sender paces it within the campaign's daily cap and active hours.

### Read replies (inbox)
```
GET https://opentweet.io/api/v1/dm-campaigns/{id}/inbox
```
Returns `{ "conversations": [{ "lead": {...}, "reply_tag": "interested|question|not_interested|opt_out", "events": [...] }] }`. Opt-outs are auto-added to the do-not-contact list.

## Evergreen Queue

The evergreen queue keeps a pool of timeless tweets and republishes them on a schedule with cooldown gaps so the same post doesn't repeat too often. Source posts stay as templates; the scheduler clones them as regular posts at the configured times. Requires an active paid subscription (not available on trial). Pro: 10 pool / 2 per day. Advanced: 999 pool / 10 per day.

### Get queue settings + pool stats
```
GET https://opentweet.io/api/v1/evergreen/settings
```
Returns: `enabled`, `posts_per_day`, `posting_times` (`["09:00","17:00"]`), `default_cooldown_days`, plus pool counts and your plan limits.

### Update queue settings
```
PUT https://opentweet.io/api/v1/evergreen/settings
Body: {
  "enabled": true,
  "posts_per_day": 2,
  "posting_times": ["09:00", "17:00"],
  "default_cooldown_days": 14
}
```
All fields optional. `posting_times` must be `"HH:mm"` strings. `default_cooldown_days` is 1–90. `posts_per_day` capped to your plan's daily limit.

### List evergreen pool
```
GET https://opentweet.io/api/v1/evergreen/posts?page=1&limit=20&paused=false
```
Filter `paused=true` or `paused=false`. Each item includes `cooldown_days`, `last_posted_at`, `times_posted`, `paused`.

### Add to evergreen pool
Mode 1 — convert an existing post:
```
POST https://opentweet.io/api/v1/evergreen/posts
Body: { "post_id": "507f1f77bcf86cd799439011", "cooldown_days": 14 }
```
Mode 2 — create a new evergreen post directly:
```
POST https://opentweet.io/api/v1/evergreen/posts
Body: {
  "text": "Timeless tweet text",
  "category": "Tips",
  "cooldown_days": 21,
  "is_thread": false,
  "media_urls": ["https://..."]
}
```

### Get / update / remove an evergreen post
```
GET    https://opentweet.io/api/v1/evergreen/posts/{id}
PUT    https://opentweet.io/api/v1/evergreen/posts/{id}    # body: { "cooldown_days": 30, "paused": true }
DELETE https://opentweet.io/api/v1/evergreen/posts/{id}    # converts back to a draft (does not hard-delete)
```
GET also returns `recent_posts` — the last 5 published clones with their X URLs.

### Evergreen publish history
```
GET https://opentweet.io/api/v1/evergreen/history?page=1&limit=20&source_id=optional
```
Lists published clones. Filter by `source_id` to see the history of a single evergreen post.

## Inspiration (Search + Repurpose)

Search X for tweets and have AI rewrite them in the user's voice. Both endpoints require an active subscription. Search has a daily cap (Pro: 50/day, Advanced: 200/day, trial: 2/day). Repurpose counts against the AI generation daily quota.

### Search inspiration tweets
```
GET https://opentweet.io/api/v1/inspiration/search?q=AI%20agents&max_results=20&sort_order=relevancy&lang=en&has_media=true&min_likes=100&min_retweets=10
```
Required: `q`. Optional filters: `max_results`, `sort_order` (`relevancy` or `recency`), `lang`, `has_media`, `min_likes`, `min_retweets`. Response includes `data` (tweets), `meta.result_count`, and `usage` (searches_used / remaining / daily_limit).

### Repurpose a tweet with AI
```
POST https://opentweet.io/api/v1/inspiration/repurpose
Body: {
  "tweet_text": "Original tweet text to remix",
  "tweet_author": "@someone",
  "instructions": "Make it punchier and add a call to action",
  "tone": "casual",
  "save_as_draft": true
}
```
Returns `repurposed.text`, `category`, `key_topics`, plus `draft.id` when `save_as_draft` is true (default). Honors the user's voice profile and content pillars automatically. Optional `x_account_id` tags the saved draft.

## Analytics

### Account overview
```
GET https://opentweet.io/api/v1/analytics/overview
```
Returns posting stats (total posts, publishing rate, active days, avg posts/week, most active day/hour, threads, media posts), streaks (current, longest), trends (this week vs last, this month vs last, best month), category breakdown, and recent activity (daily counts for last 7 and 30 days).

### Tweet engagement metrics (Advanced plan only)
```
GET https://opentweet.io/api/v1/analytics/tweets?period=30
```
Returns per-tweet engagement: likes, retweets, replies, quotes, impressions, bookmarks, engagement rate. Also includes top/worst performers, content type stats, engagement timeline, and best hours/days. Period: 7-365 days or "all".

### Best posting times
```
GET https://opentweet.io/api/v1/analytics/best-times
```
Two analysis modes:
- **`engagement_weighted`** — uses real per-tweet engagement to score every hour×day cell. Returns `heatmap`, `confidence`, `top_windows`, `best_day`, `best_hour`, `worst_day`, `worst_hour`, `insights`. Only available after running an analysis.
- **`frequency_only`** — fallback based purely on when the user has posted. Returned when no engagement profile exists yet (needs ≥3 published posts).

Both modes also return legacy `hour_distribution`, `day_distribution`, `best_hours`, `best_days` keys for backward compatibility.

### Trigger fresh best-times analysis
```
POST https://opentweet.io/api/v1/analytics/best-times/analyze
Body: {}    # optional: { "x_account_id": "..." }
```
Pulls the user's recent published tweets from X, computes engagement-weighted windows, and stores the profile. Has a built-in cooldown — if a recent analysis is still fresh, returns `429` with `next_available_at`. Returns `success`, `profile` (status `ready` / `analyzing` / `insufficient_posts`).

## Common Workflows

**First: verify your connection works:**
1. `GET /api/v1/me` — check `authenticated` is true, `subscription.has_access` is true

**Post a tweet right now (one step):**
1. `GET /api/v1/me` — check `limits.can_post` is true
2. `POST /api/v1/posts` with `{ "text": "...", "publish_now": true }`

**Post a tweet with an image:**
1. `GET /api/v1/me` — check limits
2. Upload: `POST /api/v1/upload` with the image file — get back a URL
3. Create + publish: `POST /api/v1/posts` with `{ "text": "...", "media_urls": ["<url>"], "publish_now": true }`

**Schedule a tweet:**
1. `GET /api/v1/me` — check `limits.remaining_posts_today` > 0
2. `POST /api/v1/posts` with `{ "text": "...", "scheduled_date": "2026-05-01T10:00:00Z" }` — you MUST make this HTTP call
3. Read the response JSON — confirm `posts[0].status === "scheduled"` and show the user the `id` and `scheduled_date` from the response

**Schedule a tweet with auto-retweet boost:**
1. `GET /api/v1/me` — check `limits.remaining_posts_today` > 0
2. `POST /api/v1/posts` with text, `scheduled_date`, `auto_retweet_enabled: true`, `auto_retweet_offset_minutes: 240`
3. Show the user the `id` and `scheduled_date` from the response

**Schedule a tweet with auto-plug (monetise viral reach):**
1. `GET /api/v1/me` — check `limits.remaining_posts_today` > 0
2. `POST /api/v1/posts` with text, `scheduled_date`, `auto_plug_enabled: true`, `auto_plug_threshold: 50`, `auto_plug_text: "..."`
3. Show the user the `id` and confirm auto-plug is armed
4. Once live, the scheduler fires the reply automatically when likes reach the threshold — no further action needed

**Schedule a week of content:**
1. `GET /api/v1/me` — check remaining limit
2. Bulk create: `POST /api/v1/posts` with `"posts": [...]` array, each with a scheduled_date
3. Show the user the list of created post IDs and their scheduled dates from the response

**Post a tweet with an AI-generated image:**
1. `GET /api/v1/me` — check limits
2. `POST /api/v1/generate/image` with `{ "prompt": "...", "aspect_ratio": "16:9" }` — get back a URL immediately
3. `POST /api/v1/posts` with `{ "text": "...", "media_urls": ["<url from step 2>"], "publish_now": true }`

**Post a tweet with an AI-generated video:**
1. `GET /api/v1/me` — check limits
2. `POST /api/v1/generate/video` with `{ "prompt": "...", "aspect_ratio": "16:9", "duration": 5 }` — get back a `job_id`
3. Poll `GET /api/v1/generate/video/{job_id}` every 10 seconds until `status` is `"completed"` — get the `url`
4. `POST /api/v1/posts` with `{ "text": "...", "media_urls": ["<url from step 3>"], "publish_now": true }`

**Write and schedule a long-form X Article:**
1. `GET /api/v1/me` — check `subscription.has_access` is true (articles need a paid plan)
2. Optional cover: `POST /api/v1/upload` with an image — get back a URL
3. `POST /api/v1/articles` with `{ "title": "...", "content_markdown": "# ...", "cover_image_url": "<url>", "scheduled_date": "2026-07-15T14:00:00Z" }`
4. Confirm the response has `status: "scheduled"` and show the user the article `id`
5. After publish time, `GET /api/v1/articles/{id}` — `article_url` is the live X link

**Run a DM outreach campaign (human-approved):**
1. `GET /api/v1/me` — confirm the plan is Advanced or Agency (DM Campaigns return 403 otherwise)
2. `POST /api/v1/dm-campaigns` with the ICP, sources, message brief, limits, and `accept_policy: true`
3. `POST /api/v1/dm-campaigns/{id}/discover`, then poll `GET /api/v1/dm-campaigns/{id}` until status is `reviewing`
4. `GET /api/v1/dm-campaigns/{id}/leads?state=qualified` — show the user the qualified leads and their drafted messages
5. Only after the user approves specific leads: `PATCH .../leads/{leadId}` with `action: approve`. Never auto-approve every lead.
6. `GET /api/v1/dm-campaigns/{id}/inbox` later to read replies

**Find inspiration and repurpose it:**
1. `GET /api/v1/inspiration/search?q=...&min_likes=500` — pick a tweet
2. `POST /api/v1/inspiration/repurpose` with `tweet_text`, `tweet_author`, `save_as_draft: true`
3. The saved draft's `id` can then be scheduled with `POST /api/v1/posts/{id}/schedule`

**Set up an evergreen queue from existing drafts:**
1. `PUT /api/v1/evergreen/settings` with `{ "enabled": true, "posts_per_day": 2, "posting_times": ["09:00","17:00"] }`
2. For each draft to recycle: `POST /api/v1/evergreen/posts` with `{ "post_id": "...", "cooldown_days": 14 }`
3. `GET /api/v1/evergreen/history` later to see what got published

**Tune posting times based on engagement:**
1. `POST /api/v1/analytics/best-times/analyze` — wait for `profile.status: "ready"` (poll if `analyzing`)
2. `GET /api/v1/analytics/best-times` — read `top_windows` and `best_hour` / `best_day`
3. Schedule new posts at the suggested times

## Important Rules
- ALWAYS call `GET /api/v1/me` before scheduling or publishing to check limits and connected accounts.
- For multi-account users, call `GET /api/v1/accounts` and pass `x_account_id` to target a specific account.
- CRITICAL: You MUST make the actual HTTP API call for every operation. Never skip the call and generate a response from memory or context.
- CRITICAL: Always parse and use the ACTUAL JSON response from the API. Never fabricate or assume response values.
- CRITICAL: A 4xx or 5xx HTTP status means the operation FAILED — never report success to the user on an error response.
- CRITICAL: After scheduling a post, always show the user the `id` field from the API response. If you cannot show a real 24-character MongoDB ObjectId from the response, the call was not made.
- Post IDs are always 24-character MongoDB ObjectIds (e.g. "507f1f77bcf86cd799439011"), never short strings.
- Every post response includes a `status` field: "draft", "scheduled", "posted", or "failed".
- Published posts include a `url` field with the real X post URL. Always use this URL — never construct your own.
- To verify a post was published, check: `status` is "posted" AND `url` is present.
- To verify a post was scheduled, check: `status` is "scheduled" AND `scheduled_date` is present in the response.
- Tweet max length: 280 characters (per tweet in a thread).
- Bulk limit: 50 posts per request (create or batch-schedule).
- Rate limit: 60 requests/minute, 1,000/day (Pro); 300/min, 10,000/day (Advanced).
- Dates must be ISO 8601 and in the future — past dates are rejected.
- Active subscription required to schedule, publish, use evergreen, search inspiration, or repurpose. Creating drafts is free.
- Including `scheduled_date` or `publish_now` in `POST /api/v1/posts` requires a subscription.
- Upload media before creating posts — use the returned URL in `media_urls` or `thread_media`.
- Media limits: 5MB for images (JPG, PNG, GIF, WebP), 20MB for videos (MP4, MOV).
- URL-containing posts have a separate, plan-based daily cap. A 429 with a `urlLimit` payload means the post was saved as a draft instead of published.
- Tweet engagement analytics require the Advanced plan (returns 403 on Pro).
- Evergreen queue is not available during trial.
- X Articles require a paid plan (not trial) AND X Premium (any tier) on the publishing X account. A 502 with `code: "publish_failed"` carries X's exact rejection reason.
- Article content is markdown. Do not send DraftJS or HTML — OpenTweet handles the rich-text conversion.
- `auto_retweet_offset_minutes` must be 1–10080 (up to 7 days) when `auto_retweet_enabled` is true.
- 403 = no subscription / X not connected, 429 = rate limit, daily post limit, URL post cap, or evergreen pool full.
- Check response status codes: 201=created, 200=success, 4xx=client error, 5xx=server error.

## Safety Guardrails

**Publishing is irreversible** — once a tweet is posted to X it cannot be undone via the API (DELETE removes it locally and from X, but reposts are not the same tweet).

### Confirm before publishing
- Before calling `/publish` or using `publish_now: true`, always tell the user which post(s) you are about to publish and ask for confirmation.
- Show the tweet text (truncated if long) and the post ID so the user can verify.
- The same applies to articles: before `POST /api/v1/articles/{id}/publish` or `publish_now: true` on an article, show the user the title and ask for confirmation. A published article cannot be edited.

### Scheduled posts ≠ ready to publish
- If a post has a `scheduled_date` in the future, it is meant to be published at that time by the scheduler — not right now.
- NEVER call `/publish` on a post that has a future `scheduled_date` unless the user explicitly asks you to publish it immediately.
- When the user asks to "publish" posts, clarify whether they want to publish NOW or schedule for later. Default to scheduling if dates are provided.

### Evergreen sources are not regular drafts
- A post with `isEvergreen: true` is a recurring template. The scheduler publishes clones, not the source itself.
- NEVER call `/publish` directly on an evergreen source post (the API will reject it). Add it to the queue with `POST /api/v1/evergreen/posts` and let the scheduler run.

### Batch operations — go slow
- When creating or scheduling more than 5 posts, summarize the batch (count, date range, first/last tweet previews) and ask the user to confirm before proceeding.
- Never bulk-create AND immediately publish in one go. Create as drafts or scheduled posts first, let the user review, then publish only on confirmation.
- When using batch-schedule, show the user the list of dates before sending the request.

### Don't loop publish calls
- Never loop through a list of posts calling `/publish` on each one without explicit user approval for the full list.
- If the user asks to "publish all my drafts" or similar, list them first and get confirmation.

### DM campaigns are opt-in outreach, not a blaster
- Never approve DM leads in bulk without the user reviewing them. Show the qualified leads and their drafted messages, and only approve the specific ones the user picks.
- X prohibits unsolicited automated bulk DMs. Keep the daily caps conservative, never try to bypass the pacing, and respect that approving a lead queues it for a drip send rather than firing immediately.
- Opt-outs and negative replies are auto-suppressed. Do not re-add or re-contact a suppressed profile.

### AI-generated content needs a review pass
- Before saving repurposed tweets as auto-scheduled posts, show the user the AI output and let them edit. The repurpose endpoint already saves to drafts by default — keep `save_as_draft: true` unless the user has reviewed.

## Full API docs
For complete documentation: https://opentweet.io/api/v1/docs
