Publishing to X (Twitter)
This tutorial covers connecting an X account to PowerMarketing and publishing to it — the developer app, OAuth 2.0 with PKCE, threading behaviour, rate limits, metrics, and troubleshooting.
Human action AI Agent Approval gate Live on X
Long posts become threads — they are never truncated
X allows 280 characters per post. PowerMarketing's brand voice and content pillars routinely produce more than that, so anything longer is split into a thread, each reply chained to the one before it.
content[:280] — a 600-character post published as a sentence that stopped mid-word, with the rest silently discarded and no error. If you have X posts from before this date that read as though they were cut off, that is why.
| Content length | Result |
|---|---|
| ≤ 280 characters | One post. |
| > 280 characters | A thread, split on word boundaries. Nothing is lost. |
| A single word > 280 characters | Hard-split — a URL-like token cannot break cleanly, so it is cut rather than dropped. |
The post's recorded ID and URL are those of the first post in the thread, so Posts and metrics track the thread head.
To forbid threading — for example if a brand's policy is one post or nothing — pass {"thread": false} in the publish options. Over-length content then raises a clear error instead of publishing a thread.
One-Time Setup
Step 1 — Create the X developer app
Create a project and an app
At the developer portal, create a Project, then an App inside it. The app name is shown on the consent screen, so use something your audience will recognise.
Step 2 — Configure user authentication
Open "User authentication settings"
In the app's settings, click Set up under User authentication settings.
App permissions: Read and write
Choose Read and write. The default is read-only, and publishing fails with a 403 if you leave it.
Type of App: Web App / Confidential client
Choose Web App, Automated App or Bot. This issues a Client Secret, which PowerMarketing needs — a public client will not work.
Add the callback URI
Paste the exact URL PowerMarketing shows you in Step 3:
https://marketing-api.planningpowertools.com/api/oauth/x/callback
It must match character for character. A mismatch is rejected before the consent screen even loads.
Copy the OAuth 2.0 Client ID and Secret
X shows the secret once. These are the OAuth 2.0 credentials — not the API Key/Secret, and not the Bearer Token. Using the wrong pair is the single most common setup mistake here.
Step 3 — Save them in PowerMarketing
Settings → Credentials → X (Twitter) Configuration
Paste the Client ID and Client Secret, confirm the Callback URI (use Copy to take the exact value to X), and click Save App Config. The badge turns to Configured.
Step 4 — Connect the account
Click Connect X Account
You are sent to X's consent screen. PowerMarketing requests tweet.read, tweet.write, users.read and offline.access, using PKCE — which X requires. Authorise, and you are returned to Settings with the account listed by its handle.
Publishing a post
Unlike YouTube, X content can auto-publish: post, short_post, news and the other non-locked content types follow your Publication Rules and autopilot level as normal. Only video and reel always require approval.
Tokens & refresh
X access tokens are valid for two hours. PowerMarketing renews them automatically about two minutes before expiry, so a connected account keeps working indefinitely.
The offline.access scope is what makes X issue a refresh token at all. If it is missing, the connection is rejected at setup rather than failing silently two hours later.
Rate limits & access tiers
Per X's published rate limits, the posting endpoint allows:
| Endpoint | Limit |
|---|---|
POST /2/tweets — per user | 100 per 15 minutes |
POST /2/tweets — per app | 10,000 per 24 hours |
X sells tiered access (Free, Basic, Pro, Enterprise) with different monthly caps. Those caps change from time to time, so check the current figures on X's own pricing page rather than relying on a number written here — and size your posting cadence against the tier you are actually on.
Metrics
The daily metrics job reads GET /2/tweets/{id}?tweet.fields=public_metrics and maps the result onto PowerMarketing's common shape:
| PowerMarketing | X field |
|---|---|
impressions | impression_count |
likes | like_count |
comments | reply_count |
shares | retweet_count + quote_count |
X splits amplification into reposts and quotes while the learn-loop has a single shares field, so the two are summed. Metrics are read for the thread head; replies in the thread accrue their own numbers, which are not aggregated.
Reference tables
| Item | Value |
|---|---|
| Required scopes | tweet.read, tweet.write, users.read, offline.access |
| App permissions | Read and write |
| App type | Web App / confidential client (must issue a secret) |
| Callback URI | {API_BASE}/api/oauth/x/callback |
| Authorize URL | https://x.com/i/oauth2/authorize |
| Token URL | https://api.x.com/2/oauth2/token (HTTP Basic auth) |
| Publish endpoint | POST https://api.x.com/2/tweets |
| PKCE | Required — S256 |
| Access token lifetime | 2 hours, refreshed automatically |
| Character limit | 280 per post; longer content threads |
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| 403 on publish | App permissions are still Read. Set them to Read and write, then reconnect the account — changing permissions does not upgrade tokens already issued. |
| "X did not return a refresh token" | offline.access is missing from the app's scopes. Add it and connect again. |
invalid_request at the consent screen | The callback URI does not match the one registered. Use the Copy button and paste it verbatim. |
Token exchange fails with invalid_client | You used the API Key/Secret or the Bearer Token instead of the OAuth 2.0 Client ID and Secret. |
| Publishing stops after ~2 hours | Refresh could not run — usually the Client Secret changed. Re-save the app config, then reconnect. |
| Publishing stops after working for days | The refresh token was rotated by another tool using the same account. Reconnect from PowerMarketing. |
| 403 "duplicate content" | X rejects identical posts. Edit the draft or use Regenerate. |
| "X thread broke after N segment(s)" | A mid-thread request failed, usually a rate limit. The error names the first post's URL — check the thread on X and post the remainder manually, or delete and republish. |
| Post is over 280 and you did not want a thread | Pass {"thread": false} in the publish options to get an error instead. |