Social Media

Cross-platform social media workspace for drafting, scheduling, and publishing posts to LinkedIn, X, Mastodon, Bluesky, and YouTube.

What it does

Social Media is a bundled module for cross-platform publishing: one draft can target multiple connected accounts at once, and the module handles AI-assisted drafting, image/video media, scheduling, and direct publishing per platform. It replaces the legacy linkedin bundled module — previously connected LinkedIn accounts and posts carry over. Wired platforms are LinkedIn, X, Mastodon, Bluesky, YouTube, Facebook (Pages), and Instagram (Business/Creator) (no other platforms are implemented).

The primary UI is intentionally limited to the core review workflow: Needs review, Scheduled, and Published. Account connections remain available through the compact account-management action instead of occupying a primary workspace tab; the former dashboard, analytics preview, and full strategy editor are no longer part of the primary navigation. The weekly focus stays directly above the review queue because it is the shortest path from current intent to AI-generated drafts.

Image generation offers two model choices. Settings default sends no model override and therefore follows the image provider/model configured under Settings -> ClapilotAICore. Grok Imagine sends the explicit grok-imagine-image-quality override through /api/generated-images/generate and requires a configured xAI provider. Generation stays in the existing background job flow and the resulting image is attached to the saved draft. The native Apple composer currently supports photo attachments and AI text drafting, but does not expose image generation; its list navigation receives the same three-state simplification without a non-functional model control.

The Needs review workspace includes a weekly focus prompt. It is stored separately from the durable content strategy: AI draft generation and agent review actions use the strategy as the long-term frame and the weekly prompt as the current, more specific emphasis. Saving an empty prompt clears that weekly focus. After saving a non-empty weekly focus, Regenerate drafts rewrites every post currently in draft or ready_for_review through the existing agent review action. The action asks for confirmation because it replaces draft titles and content. Scheduled, publishing, and published posts are never included; media and publish targets on regenerated drafts are preserved.

How to open / enable it

  • frontend: /modules/social-media (module navigation)
  • repo path: bundled-modules/social-media
  • module API proxy: /api/modules/social-media/api/*
  • scheduled publishing worker: bundled-modules/social-media/workers/post-publisher.mjs

The module UI renders natively in the app ("renderer": "react" in module.json, component src/components/modules/social-media-module.tsx) using the shared Clapilot design system and app-wide localization (socialMedia.* keys); index.html remains as the iframe fallback entry with feature parity.

The module is also available as a native Apple screen in clients/apple/ClapilotApple (Sources/Clapilot/Views/SocialMediaView.swift) for iPhone, iPad, and macOS. The native screen covers drafts/scheduled/published lists with search, a full-screen composer (content, internal title, target account selection with per-platform character limits, photo attachments, AI draft generation, publish now, schedule/unschedule, delete), per-target publish results with remote-post links, and account management (LinkedIn/YouTube OAuth via system browser, Mastodon/Bluesky token forms, disconnect; X accounts remain managed in the web settings). It talks to the same module API endpoints through the authenticated ClapilotAPI session and reports live page context with module slug social-media.

Key workflows

Connect accounts

Connected accounts are workspace-shared: every signed-in user sees all connected accounts (regardless of who connected them) and can publish through any of them. Each account exposes who connected it — the AccountView carries connectedBy (user id) and connectedByName (resolved from the users table, the user's e-mail address; empty string when unresolvable) — and any authenticated user may disconnect any account (DELETE accounts/{platform}/{id}; X remains managed in Settings). Reconnecting the same remote identity (LinkedIn member, Mastodon instance+account, Bluesky DID, YouTube channel) from a different user updates the existing shared row — including its stored tokens and the connectedBy user — instead of creating a duplicate. Posts, in contrast, stay per-user: each user only sees and manages their own drafts and schedules.

PlatformConnect viaText limitMedia per post
LinkedInOAuth popup from the module (admin-configured LinkedIn OAuth app)3000up to 1 image, no video (v1)
Xnative X app connection in Settings -> App-Verbindungen280up to 4 images or 1 video
Mastodoninstance URL + personal access token500up to 4 images or 1 video
Blueskyhandle + app password (optional service URL, default https://bsky.social)300up to 4 images (max 1 MB each), no video (v1)
YouTubeGoogle OAuth popup from the module (admin-configured Google OAuth app)5000 (video description; post title becomes the video title, max 100)exactly 1 video, no images
Facebook (Pages)Meta OAuth popup from the module (admin-configured Meta app); one flow connects Pages + Instagram5000up to 4 images or 1 video
Instagram (Business/Creator)own Business-Login flow (oauth/start?platform=instagram, no Facebook Page needed); page-linked accounts are also imported by the Facebook flow22001-4 images (carousel) or 1 video (Reel); media required

Content is soft-trimmed to the strictest limit of the selected targets by the connectors; the composer shows per-platform character counters so users can adjust before publishing.

LinkedIn (admin OAuth keys required):

  • OAuth client id/secret come from app_settings.linkedin_oauth_client_id / linkedin_oauth_client_secret (App Verbindungen -> Externe Integrationen, admin-only), with LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET env fallbacks.
  • Default scopes: openid profile email w_member_social (override via LINKEDIN_OAUTH_SCOPES).
  • Accounts are stored in the existing linkedin_accounts table; accounts connected through the legacy linkedin module keep working without reconnecting. Personal and company account types are supported through oauth/start?accountType=personal|company.
  • The post composer shows every target with its account display name, avatar, LinkedIn account type (personal/company), and a distinct handle where available. The same identity is retained in publish results, so users with multiple LinkedIn profiles or company pages can verify the destination while editing, scheduling, and publishing.

X: X accounts are connected and disconnected in Settings -> App-Verbindungen (table x_user_integrations, admin OAuth keys in app_settings.x_oauth_client_id/secret). The module lists the connected X account as a publish target and deep-links to Settings for connection management; DELETE accounts/x/{id} is rejected with 400.

Mastodon: POST accounts/mastodon with { instanceUrl, accessToken, label? }. The token is verified against GET {instance}/api/v1/accounts/verify_credentials before the account is stored. Users create the token themselves in their Mastodon instance settings (read/write scope).

Bluesky: POST accounts/bluesky with { handle, appPassword, serviceUrl?, label? }. Credentials are verified via com.atproto.server.createSession; the app password (not the main account password) is stored encrypted and a fresh session is created on demand when publishing.

YouTube (admin Google OAuth keys required):

  • Reuses the global Google OAuth client from app_settings.google_oauth_client_id / google_oauth_client_secret with the scopes https://www.googleapis.com/auth/youtube.upload and https://www.googleapis.com/auth/youtube.readonly. The request explicitly disables incremental scope merging so older Workspace/Drive grants are not folded into the separately stored YouTube channel credential.
  • Connect via oauth/start?platform=youtube (popup, same completion endpoint as LinkedIn). The redirect URI <publicBaseUrl>/api/modules/social-media/api/oauth/complete must be whitelisted in the Google Cloud console OAuth client; override with SOCIAL_MEDIA_GOOGLE_REDIRECT_URI when needed.
  • The account row is stored in social_media_accounts with platform = 'youtube'; the handle shows the channel title.
  • Besides the module's Konten tab, the channel can also be connected/disconnected in Settings -> App-Verbindungen via the YouTube connection card (src/components/settings/youtube-connection-card.tsx), which uses GET accounts, GET oauth/start?platform=youtube, and DELETE accounts/youtube/{id} and shows an admin hint when the Google OAuth client is missing.
  • Publishing uploads the attached video through the YouTube Data API v3 resumable upload. The post title becomes the video title (max 100 chars), the post content becomes the description (max 5000 chars), and metadata.youtubePrivacyStatus (public | unlisted | private, default public) controls visibility. A YouTube target requires exactly one attached video (youtube_requires_video per-target error otherwise); images are not supported for YouTube.

Facebook Pages + Instagram (admin Meta app required):

  • Meta App-ID/App-Secret come from app_settings.meta_oauth_client_id / meta_oauth_client_secret (App Verbindungen -> Externe Integrationen, admin-only), with META_OAUTH_CLIENT_ID / META_OAUTH_CLIENT_SECRET env fallbacks.
  • One connect flow for Pages + page-linked Instagram: GET oauth/start?platform=facebook (meta is accepted as an alias) opens the Meta consent dialog with the scopes pages_show_list, pages_read_engagement, pages_manage_posts, instagram_basic, instagram_content_publish, and business_management. On completion the module exchanges the code for a long-lived user token, loads GET /me/accounts, and creates one facebook account row per Page plus one instagram account row per linked Instagram Business account (rows in social_media_accounts, page tokens encrypted; page tokens from a long-lived user token do not expire). Upserts are keyed workspace-wide on the Page id / IG user id.
  • The redirect URI <publicBaseUrl>/api/modules/social-media/api/oauth/complete must be whitelisted under Facebook Login -> Settings -> Valid OAuth Redirect URIs in the Meta developer console; override with SOCIAL_MEDIA_META_REDIRECT_URI when needed.
  • Facebook Login for Business (new business apps): Meta business apps created since 2023 cannot request the publishing permissions via the classic scope parameter — the consent dialog fails with "Invalid Scopes: pages_manage_posts, instagram_basic, instagram_content_publish". Fix: in the Meta developer console add the product Facebook Login for Business, create a Configuration (type "General") that includes the permissions above, and paste its Configuration ID into App Verbindungen -> Externe Integrationen -> Meta Login Config ID (app_settings.meta_login_config_id, env fallback META_LOGIN_CONFIG_ID). When set, oauth/start sends config_id instead of scope; the app must also be of type Business.
  • Meta app review: while the Meta app is in development mode, only app admins/developers/testers can connect and publish. Publishing for external users requires Meta app review approval of the requested permissions.
  • Public base URL required for media: Meta fetches post media by URL. The module serves media through signed, time-limited public links (GET media/public?path=&exp=&sig=, HMAC-SHA256, default TTL 30 min) built from app_settings.public_base_url (or PUBLIC_BASE_URL). Publishing image/video posts to Facebook or Instagram fails with a clear error when no publicly reachable base URL is configured; text-only Facebook posts work without it.
  • Instagram publishing uses the Content Publishing API (media container -> status poll -> publish; carousels for 2-4 images, Reels for video). Instagram cannot publish text-only posts — a target without media fails with the stable per-target error instagram_requires_media; other targets of the same post are unaffected.

Instagram Direkt-Login (ohne Facebook-Page):

Instagram can also be connected on its own via Meta's Business Login for Instagram — no Facebook Page required. This is a separate connect flow with its own app credentials:

  • Requirements: a professional Instagram account (Business or Creator) and the App-ID/App-Secret from the Meta app's Instagram API use case (App Verbindungen -> Externe Integrationen -> Instagram App-ID / App-Secret, app_settings.instagram_app_id / instagram_app_secret, env fallbacks INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET). These are the Instagram app credentials, not the Facebook app id/secret.
  • Connect via GET oauth/start?platform=instagram → authorize dialog on https://www.instagram.com/oauth/authorize with the scopes instagram_business_basic and instagram_business_content_publish. The redirect URI <publicBaseUrl>/api/modules/social-media/api/oauth/complete must be whitelisted in the Meta console under Instagram API -> API setup with Instagram login -> Business login settings (override with SOCIAL_MEDIA_INSTAGRAM_REDIRECT_URI).
  • On completion the module exchanges the code for a short-lived token (api.instagram.com/oauth/access_token), upgrades it to a long-lived IG user token (~60 days) (graph.instagram.com/access_token, grant_type=ig_exchange_token), loads the profile via graph.instagram.com/v21.0/me, and stores the account in social_media_accounts with session_data.login_type = "instagram_login" (token encrypted, expiry tracked).
  • Token refresh: when the stored token is within 7 days of expiry, publishing automatically refreshes it via graph.instagram.com/refresh_access_token (grant_type=ig_refresh_token) and persists the new token. Refresh only works for tokens older than 24 hours; a failed refresh is tolerated (publishing continues with the current token). Accounts whose token has fully expired show status expired and must be reconnected.
  • Coexistence with the page-based flow: both connect paths coexist. Direct-login accounts publish via graph.instagram.com with the IG user token; page-linked accounts publish via graph.facebook.com with the page token. Because the two flows use different id spaces, upserts dedupe by IG user id and by handle/username — reconnecting the same IG account through the other flow updates the existing row (the most recent connect defines the publish mode) instead of creating a duplicate.

OAuth completion flow (LinkedIn, YouTube, Facebook, Instagram)

All OAuth platforms share one completion endpoint: GET oauth/complete is a public (unauthenticated) module endpoint, allowlisted in src/app/api/modules/[slug]/api/[...endpointPath]/route.ts; the OAuth state row carries the owning user and the platform (linkedin, youtube, facebook, or instagram). On completion it returns a small HTML page that posts {"type":"social-media:oauth-complete","platform":...} (module UI) — plus, for LinkedIn only, the legacy {"type":"linkedin-oauth"} message (Settings LinkedIn connection card) — to window.opener, then closes the popup.

Module UI oauth/start popup Provider consent LinkedIn / Google / Meta oauth/complete public, allowlisted postMessage popup closes

Migration note (redirect URI): the module uses a NEW OAuth redirect URI: <publicBaseUrl>/api/modules/social-media/api/oauth/complete. This URI must be added to the authorized redirect URLs of the LinkedIn developer app; the old /api/modules/linkedin/api/oauth/complete URI is no longer used. Until the new URI is whitelisted, LinkedIn connect attempts fail at the LinkedIn authorize step. The redirect URI can be overridden explicitly with the SOCIAL_MEDIA_LINKEDIN_REDIRECT_URI env variable.

Compose a post

The composer has an explicit post-type selector (persisted as metadata.postType, values content | video; posts saved before the field existed count as video when they contain a video attachment, otherwise content):

  • Text/Bild (content): image media tools only (generate image, edit image, upload images); selectable targets are LinkedIn, X, Mastodon, and Bluesky. The YouTube chip stays visible but disabled with a hint that YouTube is video-only.
  • Video (video): video media tools only (Video-Studio import, AI video generation, upload video) with at most one video; selectable targets are X, Mastodon, and YouTube. LinkedIn and Bluesky chips stay visible but disabled (no video support in v1). The YouTube visibility select and exactly-one-video validation apply as before.

Switching the type while incompatible media or targets are still attached shows a localized conflict warning and blocks save, schedule, and publish until the user resolves it — attachments and targets are never dropped automatically.

Media flows. Media files live in the module workspace at <workspace>/social-media/media/ (workspace root from CLAPILOT_WORKSPACE_DIR / OPENCLAW_WORKSPACE_DIR, default /app/workspace), with an index.json sidecar for titles/sources.

  • Generate/edit images: the module UI calls the existing app routes POST /api/generated-images/generate and POST /api/generated-images/edit client-side (authenticated browser session), then re-uploads the resulting blob via POST media/import with kind: "upload" and source: "generated"/"edited". After a user submits the prompt, the prompt dialog closes and the operation continues as a composer background job so the user can keep editing the post. Completion uses POST posts/{id}/media to append the result atomically to the post that started the job, even if another post is selected meanwhile. Attached images can be opened in a large preview from the thumbnail.
  • Video-Studio videos: POST media/import with { kind: "video-studio", slug } copies <workspace>/video-studio/videos/<slug>.mp4 directly (shared module workspace volume). The UI picker degrades gracefully when the caller may not access the video-studio module API.
  • Provider video generation (KI): the UI calls POST /api/social-media/video-generation (see below), polls GET /api/social-media/video-generation/{id} until ready, then imports with { kind: "livestream-asset", assetId }; the handler resolves livestream_assets.media_path and copies the file. Available provider rows include xai-grok-video when the xAI Grok runtime provider has OAuth/API credentials.
  • Uploads: { kind: "upload", filename, base64, mime?, title?, source? }; base64 payloads up to 80 MB server-side (the UI caps file uploads at 50 MB). Supported types: png, jpg/jpeg, webp, gif, mp4, mov, m4v, webm.
  • GET media/file?path=media/<file> serves the binary inline (path-traversal guarded, must stay inside the module media dir). DELETE media?path= removes file + index entry and refuses with 409 while a non-posted post still references the file.

AI draft generation

POST generate with { brief, tone?, platforms?, language? } returns { title, content, hashtags[] }. The prompt targets the strictest character limit of the requested platforms and instructs the model to answer in the requested language (de/en/it, default de). Generation goes through the native ClapilotAICore runtime (/internal/responses) first and falls back to the compatibility gateway on connectivity errors — the same mechanism the legacy linkedin module used (sourceType: module_social_media_ai_draft).

Composer agent action buttons call POST posts/{id}/review-action with a client request ID. The handler atomically claims one job per post, returns an already-completed result for a repeated request ID, runs draft generation through the native runtime, and conditionally applies the result only while the claimed title, content, publish targets, and job metadata are unchanged. Media-only updates can complete independently without discarding the rewrite, while a target change cancels copy generated for the previous platform set. Agent review is available only for draft and ready_for_review posts, so scheduled or approved posts cannot silently lose their workflow state. Completed jobs move the post to ready_for_review; failures restore the pre-claim status only while that job is still the latest active claim, so a late expired request cannot settle over a replacement. Conflicting user edits cancel the late result instead of being overwritten. A failed or cancelled request ID is terminal and must be retried with a new client request ID. Every normal claim atomically requires draft or ready_for_review. For an orphaned job, or one older than the shared 15-minute stale threshold, the handler first restores the recorded review status and then performs the same guarded claim. Bulk regeneration also sends the confirmed value as expectedWeeklyPrompt, and generation uses that exact snapshot. The final post update takes a short row lock on the strategy and applies the generated result only when the stored focus still matches. A changed or cleared focus discards the model result, restores the prior post status, returns weekly_focus_changed with 409, and tells the caller to abort the remaining batch. No database lock or transaction is held while the external model runs. The composer exposes a dedicated localized restart control for that recovery path while keeping approval, scheduling, publishing, deletion, media changes, and the normal agent-action controls locked.

Schedule and publish

  • POST posts/{id}/publish publishes NOW to all pending targets (or a provided subset). Each target records posted/failed plus remoteId/remoteUrl; the post ends as posted, partial, or failed.
  • POST posts/{id}/schedule ({ scheduledFor }, future ISO timestamp, requires at least one target) and POST posts/{id}/unschedule toggle scheduled state.
  • Due scheduled posts are claimed with FOR UPDATE SKIP LOCKED (concurrent-worker safe) and published by:
    • the post-publisher worker (workers/post-publisher.mjs), started by entrypoint.sh. Env: CLAPILOT_SOCIAL_MEDIA_PUBLISHER_ENABLED (default true), SOCIAL_MEDIA_PUBLISHER_POLL_SECONDS (default 30, minimum 5), SOCIAL_MEDIA_PUBLISHER_BATCH (default 5, 1-50), --once / SOCIAL_MEDIA_PUBLISHER_ONCE=true for single-shot runs.
    • an opportunistic in-handler check: any handled module API request triggers a fire-and-forget publish-due run at most once per 60 seconds per process, so scheduled posts still go out in deployments without the worker.
  • POST posts/publish-due is the service/worker entry point. It is intentionally callable without user context and operates across all users.

How the agent can drive it

The module exposes a first-class native ClapilotAICore tool family (also available to Live Voice / Realtime):

  • social_media_list_accounts
  • social_media_get_weekly_prompt
  • social_media_update_weekly_prompt
  • social_media_list_posts
  • social_media_regenerate_drafts
  • social_media_get_post
  • social_media_create_draft
  • social_media_update_post
  • social_media_attach_media
  • social_media_schedule_post
  • social_media_publish_post
  • social_media_delete_post

The runtime catalog exposes these tools under the canonical social_media family. The compatibility ids social and social-media are accepted by catalog search and expansion. Agents updating the weekly focus read it back with social_media_get_weekly_prompt before confirming the mutation.

Behavior notes:

  • Target account_id may be omitted when exactly one connected account exists for a platform; the tool proxy resolves it from GET accounts.
  • social_media_attach_media sources: generated_image (id from images_generate/images_edit), video_studio (video slug), livestream_asset (asset id from livestream_generate_video).
  • social_media_regenerate_drafts is mutation-capable and runs only after a clear user request. It pages through every draft and ready_for_review post only when a non-empty weekly focus is saved, reuses the guarded per-post review action with the confirmed focus as an expected-value guard, preserves targets/media, excludes scheduled/published posts, and reports partial failures. If the focus changes during a model call, the final compare-and-set discards that output and both UI and agent callers abort the remaining batch instead of switching to the new value.
  • x_create_post remains available for standalone X replies/quotes outside the Social Media workflow; multi-platform posting should go through the social_media_* tools.
  • When the Social Media module is open, page context activates the module action rule; a global hint routes social-posting requests to these tools from anywhere in the app.

See Agent Tool Contracts for the full contract table.

Configuration & limits

Data model

The module owns its schema via bundled migrations (bundled-modules/social-media/migrations/, tracked in module_schema_migrations under slug social-media):

  • social_media_accounts: mastodon/bluesky/youtube accounts (platform, label, handle, avatar_url, encrypted session_data). LinkedIn stays in linkedin_accounts, X stays in x_user_integrations.
  • social_media_posts: title (internal working title), content, status (draft | scheduled | publishing | posted | partial | failed), scheduled_for, published_at, targets (JSONB array), media (JSONB array), metadata.
  • social_media_oauth_states: short-lived OAuth state rows (30-minute validity, opportunistic cleanup).

targets item shape: { platform, accountId, status: "pending"|"posted"|"failed", remoteId, remoteUrl, error, postedAt }. media item shape: { id, type: "image"|"video", path: "media/<file>", mime, source: "generated"|"edited"|"upload"|"video-studio"|"provider", title, thumbnailPath }.

One-time legacy import: the first time the schema comes up with an empty social_media_posts table and a legacy linkedin_posts table exists, its rows are copied over (original ids preserved, target platform linkedin with the row's account id, metadata.legacyLinkedinImport = true). Legacy posts whose schedule is already past-due are imported as draft instead of scheduled so nothing auto-publishes stale content right after the migration.

Token storage

Mastodon tokens, Bluesky app passwords, and LinkedIn/YouTube OAuth tokens are stored AES-256-GCM encrypted inside the account row's session_data. The key material chain (LINKEDIN_TOKEN_ENCRYPTION_SECRET falling back to AUTH_SECRET) is identical to the legacy linkedin module, so previously stored LinkedIn tokens keep decrypting.

Module API endpoints

Base: /api/modules/social-media/api

MethodEndpointPurpose
GEThealthmodule health
GETbootstraplegacy contract for the Settings LinkedIn connection card (linkedin account rows + oauth config flag)
GETaccountsall connected accounts across platforms plus platforms[] info (`kind: oauth
POSTaccounts/mastodonconnect a Mastodon account ({ instanceUrl, accessToken, label? })
POSTaccounts/blueskyconnect a Bluesky account ({ handle, appPassword, serviceUrl?, label? })
DELETEaccounts/{platform}/{id}disconnect an account (mastodon/bluesky/youtube rows, linkedin via linkedin_accounts; x is rejected with 400 — managed in Settings)
DELETEaccounts/{id}legacy alias: treated as a LinkedIn account id (Settings card contract)
GETposts?status=&q=&limit=&offset=list posts (status filter, title/content search, limit 1-200, non-negative offset with deterministic ordering)
POSTpostscreate draft ({ title?, content, targets?, media?, scheduledFor? }; scheduledFor requires targets)
GETposts/{id}read one post
PATCHposts/{id}update draft fields; clearing scheduledFor reverts to draft
DELETEposts/{id}delete post (blocked with 409 while publishing)
POSTposts/{id}/publishpublish now ({ targets? } optional subset)
POSTposts/{id}/scheduleschedule ({ scheduledFor })
POSTposts/{id}/unscheduleback to draft
POSTposts/publish-dueclaim + publish due scheduled posts across users ({ limit? }, worker/service entry)
POSTgenerateAI draft ({ brief, tone?, platforms?, language? })
GETmedialist media workspace items
POSTmedia/importimport media (`kind: upload
GETmedia/file?path=serve one media binary inline
GETmedia/public?path=&exp=&sig=public, signed media link (HMAC-SHA256 over path\nexp, expiring; used by Meta to fetch post media; tampered signature → 403, expired → 400)
DELETEmedia?path=delete a media file (409 while referenced by an unpublished post)
GET/POST`oauth/start?platform=linkedinyoutube
GEToauth/completepublic OAuth completion endpoint for LinkedIn, YouTube, and Facebook/Instagram (posts social-media:oauth-complete with the platform; LinkedIn completions additionally post the legacy linkedin-oauth message)

Implementation: bundled-modules/social-media/api/handler.mjs with per-platform connectors in bundled-modules/social-media/api/connectors/*.mjs.

App API: provider video generation

  • POST /api/social-media/video-generation with { prompt, title?, model?, provider_slug?, duration_seconds? } submits a text-to-video job through the configured livestream media-generation providers, including xAI Grok Video when selected, and returns 202 { assetId, status: "generating" }.
  • GET /api/social-media/video-generation/{id} returns { assetId, status: "generating" | "ready" | "failed", mediaPath, thumbnailPath, error }. Ready assets are imported into the post via media/import kind: "livestream-asset".

Troubleshooting

  • LinkedIn/YouTube connect fails at the provider's authorize step — the new redirect URI <publicBaseUrl>/api/modules/social-media/api/oauth/complete is not whitelisted yet in the LinkedIn developer app / Google Cloud console OAuth client (the legacy /api/modules/linkedin/... URI is no longer used).
  • The X account cannot be disconnected from the module — by design: X connections are managed in Settings -> App-Verbindungen; the module's DELETE accounts/x/{id} returns 400.
  • A YouTube target fails with youtube_requires_video — YouTube posts need exactly one attached video and no images; switch the post type to Video and attach a single video.
  • Deleting media returns 409 — the file is still referenced by a draft or scheduled post; remove the attachment (or the post) first.
  • Scheduled posts do not publish — check the post-publisher worker (CLAPILOT_SOCIAL_MEDIA_PUBLISHER_ENABLED); without it, publishing still happens opportunistically on module API traffic, at most once per 60 seconds per process.