Posting
Everything goes through one method:
use Darvis\ApiX\XClient;
$result = app(XClient::class)->post(string $text, ?string $image = null, bool $dryRun = false);
The command x:post and the MCP tool call the same method, so the rules below apply to all three.
The order of checks
- The text is trimmed and must not be empty.
- The text must fit in 280 characters as X counts them, or 25,000 when the account has an X subscription (see below).
- Without
allow_links, the text must not contain a link. - The daily limit must not be reached.
- The image, when given, is read and checked.
- In a dry run the method stops here and returns a result without an id.
- The credentials must be complete.
- The image is uploaded, then the post is created with its media id.
Every check runs before X is called, also in a dry run, so a dry run fails exactly where a real post would.
Text length
X counts most Latin, Greek and Cyrillic characters as 1, other characters such as CJK and emoji as 2, and every link as 23, whatever its length. The package counts an emoji made of several code points per code point, so it may refuse a text with such an emoji slightly before X would.
X subscription
An account with a paid X subscription (Basic, Premium or Premium+) may post up to 25,000 characters through the API; without one X stops at 280. X accepts nothing else extra through this package: every tier gets the same posting features, the differences are in verification, ads and monetisation. Tell the package which subscription the posting account has:
X_SUBSCRIPTION=premium
The values are none (default), basic, premium and premium_plus. An unknown value counts as none. The limit follows everywhere: XClient::post(), x:post, the MCP tool description and the counter on the page. Read it in your own code with XConfig::subscription()->maxLength().
Set the subscription the account really has. With a paid tier configured on an account without one, the package lets a long post through and X refuses it.
What each tier offers
From the X help centre, About X Premium. Only the post length matters to this package.
| None | Basic | Premium | Premium+ | |
|---|---|---|---|---|
| Post length | 280 | 25,000 | 25,000 | 25,000 |
| Long posts through the API | no | yes | yes | yes |
| Edit a post | no | within 1 hour | within 1 hour | within 1 hour |
| Video length | about 2 min 20 s | about 3 hours, 8 GB, 1080p | about 3 hours, 8 GB, 1080p | about 3 hours, 8 GB, 1080p |
| Text formatting, bookmark folders, app icon | no | yes | yes | yes |
| Reply prioritisation | no | small | larger | largest |
| Blue checkmark and ID verification | no | no | yes | yes |
| Ads | normal | normal | about 50% fewer | none, some sponsored content |
| Creator Subscriptions, Original Content Rewards, Media Studio | no | no | yes | yes |
| SuperGrok, Grok Bot, Radar Search, Articles | no | no | no | yes |
The package posts text and one image, so video and editing do not apply. Articles cannot be posted through the API. Prices differ per country and change often; check them on X.
Images
One image per post: JPEG, PNG, GIF or WebP, at most 5 MB. Pass a local path or an http(s) URL. The type is read from the file contents, not from the name.
Links
X bills a post with a link at a much higher rate than a plain post. The package therefore refuses a text with a link unless X_ALLOW_LINKS=true. It treats as a link what X links as well: a URL with http:// or https://, anything starting with www. and a bare domain on a common top level domain such as arvid.nl or github.com/ArvidDeJong. Package names like darvis/api-x and file names like CHANGELOG.md are not links.
Daily limit
X_DAILY_LIMIT (default 10) caps the posts per day. The count lives in your default cache store under api_x:posts:<date> and only real posts count, dry runs do not. Set it to 0 to switch the limit off. app(XClient::class)->remainingToday() returns what is left, or null without a limit.
Dry runs
Pass dryRun: true, use --dry-run on the command, dry_run on the MCP tool, or set X_DRY_RUN=true to make every post a dry run. Reading mentions is not affected by X_DRY_RUN.
To post a reply, pass replyTo; see Mentions and replies.
The result
post() returns a Darvis\ApiX\PostResult:
| Property or method | Value |
|---|---|
text | The text as it was sent, trimmed. |
dryRun | true when nothing was sent. |
id | The id of the post, null in a dry run. |
mediaId | The id of the uploaded image, or null. |
remainingToday | Posts left today after this one, null without a limit. |
url() | https://x.com/i/web/status/<id>, or null in a dry run. |
Failures
Every failure is a Darvis\ApiX\Exceptions\XException with a message that says what to change. When X refuses a request, the message holds the HTTP status and the reason X gave. See Troubleshooting.