PPact
Productivity

X (Twitter)

Connect an X account to Pact's social studio — OAuth 2.0 PKCE, posts with images, GIFs or video, replies, mentions in the inbox, and per-tweet analytics, with X's per-request billing spelled out.

X (Twitter)

X is one of Pact's social studio providers. Once connected, you can publish and schedule posts with images, a GIF or a video, reply to posts, see mentions in the social inbox, pull per-post analytics, and delete posts — all through the same /v1/social/* routes that drive LinkedIn, Threads, Bluesky, and the rest. The provider talks to the X v2 API using OAuth 2.0 with PKCE.

X bills your X developer app for every request

Since February 2026 X charges new developer apps per request — about $0.015 per post created and $0.005 per post read — to whichever X app's credentials are used. Pact therefore reads mentions only with your workspace's own X app (see below), and only asks X for mentions newer than the last one it stored.

What works today

CapabilityStatus
Connect via OAuth 2.0 (PKCE)Live — authorize_url + complete_oauth + token refresh
Publish a postLive — POST /2/tweets. Over 280 characters is refused in the composer; Pact never cuts a post
Images, GIF, videoLive — up to 4 images (5 MB each), or one GIF (15 MB), or one video (MP4/MOV, up to 512 MB in Pact), uploaded through X's v2 chunked media endpoints before the post is created, so a failed upload never leaves a half-made post
Reply to a postLive — from the social inbox, with a live 280-character count
Mentions in the inboxLive with your own X app — GET /2/users/{id}/mentions, polled at most every 15 minutes
Per-post analyticsLive — reads public_metrics (impressions, likes, replies, retweets)
Delete a postLive — deletes on-platform and soft-deletes in Pact
Likes / reposts per postNot polled — each read is billed per post by X
Direct messagesNot supported — requires a paid tier above Basic

Attaching media

Paste an image or video link under Attachments in the composer. Before you schedule, the per-channel list says what X will do with the files; when you schedule, Pact checks each file's real type and size and refuses anything X would reject — with X's limit in the sentence. At publish time Pact fetches the file (public http(s) addresses only; private and internal addresses are refused), uploads it in 4 MB segments, waits for X to finish processing a video (up to three minutes), and then creates the post.

Media upload needs X's media.write permission. An account connected before Pact asked for it is marked in the composer — reconnect it in Social settings to attach media. Text posts keep working either way.

Connecting an account

Account connect/disconnect is admin/owner-only (it touches the credential store); any user can create posts once an account is connected.

bash
# Start OAuth — returns an authorize_url to send the admin to
curl -s -X POST "https://api.pact.place/v1/social/twitter/connect" \
  -H "Authorization: Bearer $PACT_API_KEY"

The callback (GET /v1/social/twitter/callback) completes the PKCE exchange and redirects back to the app. Tokens are stored in the credential store, never in audit payloads. Every connect/disconnect/refresh writes an audit event (social.account.connected, .disconnected, .token_refreshed, …).

Bring your own X app

X gates the v2 API behind paid tiers. Pact treats X as opt-in: the Connect button surfaces a "requires X Basic tier (~$100/mo)" notice, and you supply your own X app's client_id / client_secret — resolved per tenant from the credential store (credential key oauth_app_twitter), or from TWITTER_CLIENT_ID / TWITTER_CLIENT_SECRET as a fallback.

  • Basic (~$100/mo): 100k reads + 50k writes per month — the practical floor.
  • Pro ($5k/mo): higher quotas plus real-time Filtered Stream.
  • Free: 1,500 posts / 24h, no Filtered Stream.

The default OAuth scopes are tweet.read, tweet.write, users.read, media.write, and offline.access.

Mentions need your own X app. Pact polls X mentions only for workspaces that saved their own oauth_app_twitter credential, because every post read is billed to the app that makes it. Without one, X posting and replies work and the inbox shows no X mentions; the poller records why rather than silently returning nothing. If your X plan does not include reads, X answers 403 and Pact records that as the reason — it never reads as "no new mentions".

Rate limits are surfaced, not swallowed

When X returns a 429, the provider raises a rate-limit error carrying the time until x-rate-limit-reset so the scheduler backs off (up to three attempts) rather than hammering the API. A 403 is shown in X's own words — a duplicate post, a missing permission — not as a sign-in failure.