MediaRouter / docs /social-x-publishing.md
basyx's picture
Upload 258 files
a44271f verified
|
Raw
History Blame Contribute Delete
11.4 kB

X publishing (Phase 5B)

Phase 5B publishes text, image, animated GIF, and video posts for connected X users through the existing MediaRouter social architecture. It uses only the official X API v2 and does not add an X-specific REST, MCP, SDK, n8n, token, scheduler, retry, or job architecture.

The API contract was re-verified on 2026-08-01 against the official X documentation and live OpenAPI version 2.167:

X API v2 is the current API. No scraping, unofficial endpoint, OAuth 1.0a fallback, or legacy v1.1 media endpoint is used.

Architecture and credentials

frontend / REST / MCP / TypeScript SDK / Python SDK / n8n
                              |
                         SocialService
                              |
                   PublishingService / Scheduler
                              |
                         durable SocialJob
                              |
                         SocialPublisher
                              |
                 OAuthService -> TokenService
                              |
                          XProvider
                              |
                    official X API v2

All requests remain under /v1/social. TokenService is the only credential reader. X access and refresh tokens are never serialized to REST, frontend, browser storage, MCP, n8n, either SDK, logs, exceptions, provider metadata, or audit records.

Configure the backend only:

X_CLIENT_ID=
X_CLIENT_SECRET=
X_REDIRECT_URI=https://api.example.com/v1/social/accounts/x/callback
X_PUBLISHING_ENABLED=false
X_UPLOAD_CHUNK_BYTES=5242880
X_REQUEST_TIMEOUT_SECONDS=60
X_MEDIA_PROCESSING_POLL_SECONDS=5
X_MEDIA_PROCESSING_TIMEOUT_SECONDS=300

X_CLIENT_SECRET must never use a frontend-visible environment prefix. The redirect URI must exactly match the confidential Web App callback registered in the X Developer Console.

X_PUBLISHING_ENABLED is a fail-closed operator assertion. Set it to true only after confirming that the X project currently has the paid access, credits, write permission, and v2 media endpoint entitlement required by the deployment. OAuth account discovery remains available while this setting is false, but publishing, scheduling, media, and deletion capabilities are not advertised.

Authorization and access

A normal Phase 5A connection still requests only tweet.read, users.read, and offline.access. Publishing scopes are not silently added. The user must choose Enable publishing, or call the common connect route with authorization_purpose=publishing, to explicitly request:

Scope Use
tweet.write Create and delete posts
media.write Upload images, animated GIFs, and videos

Existing read-only connections return SOCIAL_PERMISSION_DENIED until this explicit authorization is completed. The Developer Console project must also have the current paid access, credits, endpoint entitlement, and spending limits required by X. A media endpoint that is unavailable to the project's access tier returns SOCIAL_CAPABILITY_UNSUPPORTED; MediaRouter does not pretend the upload succeeded.

Current X pricing and access tiers can change independently of a MediaRouter deployment. Confirm the Developer Console and official pricing page before production launch.

Text and replies

MediaRouter accepts a strict x metadata object. Unknown fields fail closed:

{
  "targets": [{
    "social_account_id": "<x-account-id>",
    "caption": {"text": "Release complete"},
    "x": {
      "text": "Release complete",
      "reply": {
        "in_reply_to_tweet_id": "1890123456789012345",
        "auto_populate_reply_metadata": true
      }
    }
  }],
  "publish_mode": "draft"
}

The supported contract is text plus the official reply fields shown above. IDs must be 1–19 ASCII digits. The conservative MediaRouter text limit is 280 Unicode code points; X remains authoritative for its weighted character and account-specific limits. A post may be text-only, media-only, or contain both, but it must contain text or a workspace-owned media asset.

Quote posting is deliberately absent. The current Create Post documentation marks quote_tweet_id as Enterprise-only, so self-serve MediaRouter clients cannot submit it as an arbitrary provider payload.

Images, animated GIFs, and video

MediaRouter resolves only a workspace-owned MediaAsset/MediaVariant, re-probes the file at worker time, checks that its bytes have not changed, and validates provider compatibility before any X request. It does not silently modify media. Use the existing FFmpeg/template pipeline to create a compatible variant when validation fails.

Supported contract:

Media Local validation Upload workflow
JPEG, PNG, WEBP MIME/codec agreement, readable file, dimensions, unchanged size, at most 5 MB POST /2/media/upload one-shot API
Animated GIF GIF MIME/codec, at most 15 MB, at most 1280Γ—1080, at most 350 frames and 300 million total pixels when reported by FFprobe initialize β†’ chunk append β†’ finalize β†’ status
MP4 video MP4/H.264, 0.5–140 seconds, 32Γ—32 through 1280Γ—1024, 60 FPS maximum, 1:3–3:1 aspect ratio, YUV 4:2:0, square pixels, progressive scan, AAC-LC mono/stereo, at most 512 MB initialize β†’ chunk append β†’ finalize β†’ status

The current SocialPostCreate model references one primary media asset, so Phase 5B exposes one X attachment. X itself can accept up to four photos; that larger multi-asset contract is not advertised until MediaRouter's existing post-media model exposes an ordered multi-asset request safely.

Small images are bounded before their base64 one-shot body is read. GIF and video files are never loaded wholly into memory. The worker reads configurable chunks, persists the next segment after each accepted append, heartbeats its job lease, finalizes once, and polls official processing status. Upload state is encrypted in SocialJob.provider_state_encrypted and omitted from all job views.

Publishing lifecycle and status

The shared worker uses the existing lifecycle:

QUEUED -> PREPARING -> PROCESSING -> UPLOADING -> PUBLISHING -> PUBLISHED
                                      |              |
                                      +-> RETRYING <-+
                                              |
                                            FAILED

After media processing completes, the adapter sends the typed body to POST /2/tweets, persists the returned post ID and public URL, then confirms it with GET /2/tweets/{id}. Status is normalized as PUBLISHED, DELETED, or UNAVAILABLE; provider-controlled metadata passes through the common credential redactor.

Deletion uses DELETE /2/tweets/{id} only after tenant-scoped post, target, account, capability, and external-ID checks. A 200 response must contain data.deleted=true; 404 is treated as idempotent already-deleted success.

Scheduling

Phase 5B does not claim native X scheduling. native_scheduling is false. When scheduled_publish is available, MediaRouter stores the offset-aware input as canonical UTC while preserving the supplied IANA timezone. The existing scheduler queues the same X publishing job when due.

Idempotency and crash recovery

The existing (workspace_id, Idempotency-Key) post and job constraints ensure that repeated requests with the same payload create one logical post/job. The same key with a different payload returns SOCIAL_IDEMPOTENCY_CONFLICT. Retries reuse the same job and encrypted upload state.

The X Create Post endpoint does not expose a provider idempotency key. Before creation, MediaRouter durably stores the publish start time and uploaded media identity. After an ambiguous timeout or a provider success followed by local database failure, recovery queries the authenticated user's official timeline from that time and matches exact text, reply reference, and media keys. It pages through bounded results and fails safely if it cannot exhaust the window; it does not blindly create a second post. Once found, the external ID is persisted and normal status reconciliation resumes.

Retries and errors

The shared retry service applies bounded backoff and jitter without creating a new logical job:

X result MediaRouter result Retry
401 / invalid or expired token SOCIAL_REAUTH_REQUIRED; one TokenService-managed refresh attempt once after refresh
403 / insufficient scope SOCIAL_PERMISSION_DENIED no
404 from posting/media endpoint access tier SOCIAL_CAPABILITY_UNSUPPORTED no
429 SOCIAL_RATE_LIMITED yes, bounded
408, 500, 502, 503, 504, transport timeout temporary provider failure yes, bounded
400/409 invalid post metadata SOCIAL_PUBLISH_FAILED no
400/413/415/422 invalid media SOCIAL_MEDIA_INVALID no

Provider response details are not reflected into public errors because they may contain request fragments or credentials.

Frontend, MCP, n8n, and SDKs

The frontend discovers X accounts and capabilities from the backend. It supports text, optional media, optional reply ID, publish, MediaRouter scheduling, job status, reconnect, disconnect, and normalized errors. It does not assume X capabilities when the backend reports them unavailable.

The existing MCP tools and MediaRouter Social n8n node call SocialService through the generic social API. The node supports JSON post creation, batches, Continue On Fail, scheduling, publishing, and idempotency without storing X credentials. TypeScript exports XPostMetadata/XReplyMetadata; Python exports matching TypedDict contracts. client.social remains canonical.

Limitations

  • A configured project may still lack X media endpoint entitlement, credits, or sufficient access. X_PUBLISHING_ENABLED=true records an operator's verification; it cannot grant provider access or override X policy.
  • Phase 5B deliberately supports one attachment, no polls, no community posts, no direct-message deep links, no paid-partnership fields, no long posts, and no Enterprise quote posting.
  • X has no native provider idempotency key for Create Post. Recovery is built from the official user timeline. MediaRouter durably marks the submission before calling X and never resubmits an uncertain Create Post request; it retries bounded reconciliation and fails safe when it cannot prove the outcome.
  • Analytics certification is outside Phase 5B.