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.
| Platform | Connect via | Text limit | Media per post |
|---|---|---|---|
| OAuth popup from the module (admin-configured LinkedIn OAuth app) | 3000 | up to 1 image, no video (v1) | |
| X | native X app connection in Settings -> App-Verbindungen | 280 | up to 4 images or 1 video |
| Mastodon | instance URL + personal access token | 500 | up to 4 images or 1 video |
| Bluesky | handle + app password (optional service URL, default https://bsky.social) | 300 | up to 4 images (max 1 MB each), no video (v1) |
| YouTube | Google 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 + Instagram | 5000 | up 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 flow | 2200 | 1-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), withLINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRETenv fallbacks. - Default scopes:
openid profile email w_member_social(override viaLINKEDIN_OAUTH_SCOPES). - Accounts are stored in the existing
linkedin_accountstable; accounts connected through the legacylinkedinmodule keep working without reconnecting. Personal and company account types are supported throughoauth/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_secretwith the scopeshttps://www.googleapis.com/auth/youtube.uploadandhttps://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/completemust be whitelisted in the Google Cloud console OAuth client; override withSOCIAL_MEDIA_GOOGLE_REDIRECT_URIwhen needed. - The account row is stored in
social_media_accountswithplatform = 'youtube'; the handle shows the channel title. - Besides the module's Konten tab, the channel can also be connected/disconnected in
Settings -> App-Verbindungenvia the YouTube connection card (src/components/settings/youtube-connection-card.tsx), which usesGET accounts,GET oauth/start?platform=youtube, andDELETE 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, defaultpublic) controls visibility. A YouTube target requires exactly one attached video (youtube_requires_videoper-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), withMETA_OAUTH_CLIENT_ID/META_OAUTH_CLIENT_SECRETenv fallbacks. - One connect flow for Pages + page-linked Instagram:
GET oauth/start?platform=facebook(metais accepted as an alias) opens the Meta consent dialog with the scopespages_show_list,pages_read_engagement,pages_manage_posts,instagram_basic,instagram_content_publish, andbusiness_management. On completion the module exchanges the code for a long-lived user token, loadsGET /me/accounts, and creates onefacebookaccount row per Page plus oneinstagramaccount row per linked Instagram Business account (rows insocial_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/completemust be whitelisted under Facebook Login -> Settings -> Valid OAuth Redirect URIs in the Meta developer console; override withSOCIAL_MEDIA_META_REDIRECT_URIwhen needed. - Facebook Login for Business (new business apps): Meta business apps created since 2023 cannot request
the publishing permissions via the classic
scopeparameter — 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 intoApp Verbindungen -> Externe Integrationen -> Meta Login Config ID(app_settings.meta_login_config_id, env fallbackMETA_LOGIN_CONFIG_ID). When set,oauth/startsendsconfig_idinstead ofscope; 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 fromapp_settings.public_base_url(orPUBLIC_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 fallbacksINSTAGRAM_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 onhttps://www.instagram.com/oauth/authorizewith the scopesinstagram_business_basicandinstagram_business_content_publish. The redirect URI<publicBaseUrl>/api/modules/social-media/api/oauth/completemust be whitelisted in the Meta console under Instagram API -> API setup with Instagram login -> Business login settings (override withSOCIAL_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 viagraph.instagram.com/v21.0/me, and stores the account insocial_media_accountswithsession_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 statusexpiredand must be reconnected. - Coexistence with the page-based flow: both connect paths coexist. Direct-login accounts publish via
graph.instagram.comwith the IG user token; page-linked accounts publish viagraph.facebook.comwith 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.
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/completeURI 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 theSOCIAL_MEDIA_LINKEDIN_REDIRECT_URIenv 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/generateandPOST /api/generated-images/editclient-side (authenticated browser session), then re-uploads the resulting blob viaPOST media/importwithkind: "upload"andsource: "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 usesPOST posts/{id}/mediato 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/importwith{ kind: "video-studio", slug }copies<workspace>/video-studio/videos/<slug>.mp4directly (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), pollsGET /api/social-media/video-generation/{id}untilready, then imports with{ kind: "livestream-asset", assetId }; the handler resolveslivestream_assets.media_pathand copies the file. Available provider rows includexai-grok-videowhen 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}/publishpublishes NOW to all pending targets (or a provided subset). Each target recordsposted/failedplusremoteId/remoteUrl; the post ends asposted,partial, orfailed.POST posts/{id}/schedule({ scheduledFor }, future ISO timestamp, requires at least one target) andPOST posts/{id}/unscheduletoggle 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 byentrypoint.sh. Env:CLAPILOT_SOCIAL_MEDIA_PUBLISHER_ENABLED(defaulttrue),SOCIAL_MEDIA_PUBLISHER_POLL_SECONDS(default 30, minimum 5),SOCIAL_MEDIA_PUBLISHER_BATCH(default 5, 1-50),--once/SOCIAL_MEDIA_PUBLISHER_ONCE=truefor single-shot runs. - an opportunistic in-handler check: any handled module API request triggers a fire-and-forget
publish-duerun at most once per 60 seconds per process, so scheduled posts still go out in deployments without the worker.
- the post-publisher worker (
POST posts/publish-dueis 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_accountssocial_media_get_weekly_promptsocial_media_update_weekly_promptsocial_media_list_postssocial_media_regenerate_draftssocial_media_get_postsocial_media_create_draftsocial_media_update_postsocial_media_attach_mediasocial_media_schedule_postsocial_media_publish_postsocial_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_idmay be omitted when exactly one connected account exists for a platform; the tool proxy resolves it fromGET accounts. social_media_attach_mediasources:generated_image(id fromimages_generate/images_edit),video_studio(video slug),livestream_asset(asset id fromlivestream_generate_video).social_media_regenerate_draftsis mutation-capable and runs only after a clear user request. It pages through everydraftandready_for_reviewpost 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_postremains available for standalone X replies/quotes outside the Social Media workflow; multi-platform posting should go through thesocial_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, encryptedsession_data). LinkedIn stays inlinkedin_accounts, X stays inx_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
| Method | Endpoint | Purpose |
|---|---|---|
| GET | health | module health |
| GET | bootstrap | legacy contract for the Settings LinkedIn connection card (linkedin account rows + oauth config flag) |
| GET | accounts | all connected accounts across platforms plus platforms[] info (`kind: oauth |
| POST | accounts/mastodon | connect a Mastodon account ({ instanceUrl, accessToken, label? }) |
| POST | accounts/bluesky | connect a Bluesky account ({ handle, appPassword, serviceUrl?, label? }) |
| DELETE | accounts/{platform}/{id} | disconnect an account (mastodon/bluesky/youtube rows, linkedin via linkedin_accounts; x is rejected with 400 — managed in Settings) |
| DELETE | accounts/{id} | legacy alias: treated as a LinkedIn account id (Settings card contract) |
| GET | posts?status=&q=&limit=&offset= | list posts (status filter, title/content search, limit 1-200, non-negative offset with deterministic ordering) |
| POST | posts | create draft ({ title?, content, targets?, media?, scheduledFor? }; scheduledFor requires targets) |
| GET | posts/{id} | read one post |
| PATCH | posts/{id} | update draft fields; clearing scheduledFor reverts to draft |
| DELETE | posts/{id} | delete post (blocked with 409 while publishing) |
| POST | posts/{id}/publish | publish now ({ targets? } optional subset) |
| POST | posts/{id}/schedule | schedule ({ scheduledFor }) |
| POST | posts/{id}/unschedule | back to draft |
| POST | posts/publish-due | claim + publish due scheduled posts across users ({ limit? }, worker/service entry) |
| POST | generate | AI draft ({ brief, tone?, platforms?, language? }) |
| GET | media | list media workspace items |
| POST | media/import | import media (`kind: upload |
| GET | media/file?path= | serve one media binary inline |
| GET | media/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) |
| DELETE | media?path= | delete a media file (409 while referenced by an unpublished post) |
| GET/POST | `oauth/start?platform=linkedin | youtube |
| GET | oauth/complete | public 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-generationwith{ 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 returns202 { 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 viamedia/importkind: "livestream-asset".
Troubleshooting
- LinkedIn/YouTube connect fails at the provider's authorize step — the new redirect URI
<publicBaseUrl>/api/modules/social-media/api/oauth/completeis 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'sDELETE 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.
