{"openapi":"3.1.0","info":{"title":"PostOnce Public API","version":"1.0.0-alpha","description":"Public API for PostOnce paid users"},"servers":[{"url":"https://postonce.to/api/public"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"Scoped API key issued in PostOnce Settings. OAuth authorization grants are not currently supported.","x-scopes":{"accounts:read":"List and read connected accounts.","accounts:write":"Connect and disconnect social accounts.","media:read":"Read uploaded media.","media:write":"Upload media.","posts:read":"Read posts and unpublished drafts.","posts:write":"Create, update, publish, cancel, retry, and delete posts and drafts.","workflows:read":"List and read crossposting workflows.","workflows:write":"Create, update, and delete crossposting workflows."}}},"parameters":{"accountId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connected account ID."},"mediaId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Encoded `media_id` returned by the upload endpoint."},"draftId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Draft ID."},"postId":{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Aggregate post ID."},"targetPostId":{"name":"targetId","in":"path","required":true,"schema":{"type":"string"},"description":"Concrete destination row ID from `data.targets[].target_post_id`."},"workflowId":{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Workflow ID."},"platform":{"name":"platform","in":"path","required":true,"schema":{"type":"string","enum":["bluesky","facebook","farcaster","nostr","telegram","instagram","linkedin","pinterest","reddit","snapchat","threads","tiktok","twitter","vimeo","youtube"]},"description":"Social platform to connect."},"listLimit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Maximum number of items to return."},"listAfter":{"name":"after","in":"query","required":false,"schema":{"type":"string"},"description":"Cursor returned by a previous list response."}},"schemas":{"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","required":["code","message"],"properties":{"hint":{"type":"string","description":"Suggested action to resolve the error."},"code":{"type":"string","example":"workflow_limit_reached"},"message":{"type":"string","example":"You have reached the maximum number of workflows for your plan."}}}}},"AuthErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"hint":{"type":"string","description":"Suggested action to resolve the error."},"code":{"type":"string","example":"invalid_api_key"},"message":{"type":"string","example":"Invalid API key."}}}}},"PostAnalytics":{"type":"object","properties":{"range":{"type":"object","properties":{"days":{"type":"integer"},"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"}}},"accounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string"},"username":{"type":["string","null"]},"metrics_status":{"type":"string","description":"ok, reconnect (reconnect the account to read its numbers), limited (some numbers need a permission the account lacks) or unavailable (the platform request failed; try later)."},"posts_with_views":{"type":"integer"},"median_views":{"type":["number","null"],"description":"Median views of the account's posts in the range; null with fewer than 3 posts that have views."},"followers":{"type":["integer","null"],"description":"Latest stored follower count (YouTube subscribers). Measured when the account connects, then weekly on paid plans."},"followers_observed_at":{"type":["string","null"],"format":"date-time"}}}},"posts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"post_id":{"type":["string","null"],"description":"The ID to pass to GET /v1/posts/{id}/analytics. null for posts made outside PostOnce."},"title":{"type":"string","description":"First line of the caption, at most 90 characters."},"caption":{"type":"string","description":"Full post text, including hashtags. For YouTube videos posted outside PostOnce, the video title."},"thumbnail_url":{"type":["string","null"],"description":"Cover image. Some platform links expire after a few hours."},"media_url":{"type":["string","null"],"description":"The video or image file PostOnce published, so an AI or tool can watch or view it. null for posts made directly on the platform."},"media_type":{"type":"string","enum":["video","photo"]},"published_at":{"type":"string","format":"date-time"},"origin":{"type":"string","description":"postonce when at least one copy went out through PostOnce; native when posted directly on the platform."},"destinations":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"account_id":{"type":"string"},"username":{"type":["string","null"]},"url":{"type":["string","null"]},"published_at":{"type":"string","format":"date-time"},"views":{"type":["integer","null"]},"likes":{"type":["integer","null"]},"comments":{"type":["integer","null"]},"shares":{"type":["integer","null"]},"saves":{"type":["integer","null"]},"engagement_rate":{"type":["number","null"],"description":"(likes + comments + shares + saves) / views, from the numbers the platform reports. null without views."},"views_vs_account_median":{"type":["number","null"],"description":"This copy's views divided by its account's median views in the range."}}}}}}},"truncated":{"type":"boolean","description":"More posts existed than numbers were fetched for."},"refreshing":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"}}},"PostTargetAnalytics":{"type":"object","properties":{"id":{"type":"string"},"media_url":{"type":["string","null"],"description":"The video or image file PostOnce published."},"targets":{"type":"array","items":{"type":"object","properties":{"account_id":{"type":"string"},"platform":{"type":"string"},"platform_post_id":{"type":["string","null"]},"url":{"type":["string","null"]},"published_at":{"type":"string","format":"date-time"},"metrics_status":{"type":"string","description":"ok, reconnect, limited or unavailable, as in GET /v1/analytics."},"views":{"type":["integer","null"]},"likes":{"type":["integer","null"]},"comments":{"type":["integer","null"]},"shares":{"type":["integer","null"]},"saves":{"type":["integer","null"]},"engagement_rate":{"type":["number","null"],"description":"(likes + comments + shares + saves) / views. null without views."},"last_updated":{"type":["string","null"],"format":"date-time","description":"When these numbers were read from the platform; null when the platform returned none."}}}}}},"Account":{"type":"object","properties":{"id":{"type":"string"},"platform":{"type":"string","enum":["bluesky","facebook","farcaster","nostr","telegram","instagram","linkedin","pinterest","reddit","snapchat","threads","tiktok","twitter","vimeo","youtube"],"example":"twitter"},"username":{"type":"string","example":"nigelyong"},"avatar_url":{"type":"string","nullable":true},"status":{"type":"string","enum":["active","auth_required","disconnected"],"example":"active"},"capabilities":{"type":"object","nullable":true,"description":"Discoverable account capabilities used by both the public API and generated MCP tools.","properties":{"connection_mode":{"type":"string"},"destination":{"type":"boolean"},"source":{"type":"boolean"},"backfill":{"type":"boolean"},"text":{"type":"boolean"},"images":{"type":"boolean"},"video":{"type":"boolean"},"replies":{"type":"boolean"},"threads":{"type":"boolean"}}},"media_requirements":{"type":"object","nullable":true,"description":"Media input constraints from PostOnce's publishing validator. Check required_media before a text-only post. Platform-level guidance is not proof of current account authorization or delivery success. Format conversions remain subject to processing limits.","properties":{"supported_formats":{"type":"array","items":{"type":"string"}},"convertible_formats":{"type":"object","additionalProperties":{"type":"string"}},"max_size_bytes":{"type":"object","properties":{"image":{"type":"integer"},"video":{"type":"integer"},"gif":{"type":"integer"}}},"file_constraints":{"type":"object","description":"maxFiles, maxVideoFiles, maxImageFiles, optional maxGifFiles, allowMixedMedia and optional gifBehavior.","additionalProperties":true},"required_media":{"type":"object","nullable":true,"description":"When present: minFiles and acceptedTypes (image/video) define mandatory media, with errorCode and errorMessage.","additionalProperties":true},"max_hashtags":{"type":"integer","minimum":0,"nullable":true},"min_video_duration_seconds":{"type":"number","nullable":true},"max_video_duration_seconds":{"type":"number","nullable":true},"aspect_ratio":{"type":"object","nullable":true,"additionalProperties":true},"thumbnail":{"type":"object","nullable":true,"additionalProperties":true}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ConnectAccountResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"hosted_url":{"type":"string","nullable":true,"example":"https://postonce.to/dashboard/accounts?connect=twitter&source=public_api"},"authorization_url":{"type":"string","nullable":true,"description":"Five-minute browser connection URL bound to the API-key profile with signed state. OAuth platforms return a direct provider URL; Bluesky returns a PostOnce-hosted app-password form; Farcaster returns a PostOnce-hosted managed-signer approval flow; Nostr returns a PostOnce-hosted NIP-46 remote-signer flow; Telegram returns a PostOnce-hosted flow that adds @PostOnceBot as a channel admin.","example":"https://www.tiktok.com/v2/auth/authorize/?client_key=..."},"mode":{"type":"string","enum":["credential_form_url","managed_signer","remote_signer","oauth_authorization_url"],"example":"oauth_authorization_url"},"platform":{"type":"string","example":"twitter"}}}}},"DeleteResponse":{"type":"object","properties":{"data":{"type":"object","properties":{"deleted":{"type":"boolean","example":true},"id":{"type":"string"},"provider_revocation_required":{"type":"boolean","nullable":true,"description":"True when local PostOnce access is removed but the provider requires the user to revoke the remote signer separately."},"provider_revocation_url":{"type":"string","nullable":true},"message":{"type":"string","nullable":true}}}}},"Media":{"type":"object","properties":{"id":{"type":"string"},"path":{"type":"string"},"public_url":{"type":"string"}}},"MediaInput":{"type":"object","required":["url"],"properties":{"url":{"type":"string","description":"Required absolute HTTP or HTTPS media URL. Surrounding whitespace is trimmed. Invalid URLs reject the entire request with invalid_media; URL contents are not fetched during validation.","example":"https://cdn.example.com/launch-image.png"},"type":{"type":"string","enum":["image","video"],"example":"image"},"width":{"type":"integer"},"height":{"type":"integer"},"size":{"type":"integer"},"duration":{"type":"number","description":"Video duration in seconds when known."},"thumbnail_url":{"type":"string","description":"Cover image URL for video media (JPEG or PNG, 8 MB or less). It is applied as the video cover on Instagram Reels and Stories, Facebook Reels, and TikTok (inserted as the video's first frame), and is also used in scheduled and published post previews."}}},"DraftMediaInput":{"type":"object","additionalProperties":false,"required":["type","url"],"properties":{"url":{"type":"string"},"type":{"type":"string","enum":["image","video"]},"file_name":{"type":"string"},"mime_type":{"type":"string"},"width":{"type":"integer","minimum":0},"height":{"type":"integer","minimum":0},"size":{"type":"integer","minimum":0},"duration":{"type":"number","minimum":0},"thumbnail_url":{"type":"string","nullable":true}}},"Draft":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"candidate_angle":{"type":"string"},"media":{"type":"array","items":{"$ref":"#/components/schemas/DraftMediaInput"}},"selected_account_ids":{"type":"array","uniqueItems":true,"items":{"type":"string","format":"uuid"}},"suggested_platforms":{"type":"array","items":{"type":"string"}},"source_type":{"type":"string"},"status":{"type":"string","enum":["inbox","ready","scheduled","archived"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PostTargetInput":{"type":"object","required":["account_id"],"properties":{"account_id":{"type":"string"},"content_override":{"type":"string","nullable":true},"platform_options":{"type":"object","additionalProperties":true,"nullable":true,"properties":{"ai_generated":{"type":"boolean","description":"Requests the target platform's native AI-generated or synthetic-media disclosure. Supported for TikTok direct videos, YouTube videos, and Instagram media. Facebook Page publishing does not currently expose an equivalent API field."},"channel_id":{"type":"string","description":"Farcaster channel ID for the cast."},"video_cover_timestamp_ms":{"type":"integer","minimum":0,"description":"TikTok direct video posts only. Selects the frame at this position, in milliseconds, as the video cover. When set, it takes precedence over media[].thumbnail_url for that TikTok target. Not supported for photo posts or draft uploads."},"title":{"type":"string","description":"Title for platforms that support one. For Vimeo, 1-128 characters; defaults to the first line of the content."},"privacy_view":{"type":"string","enum":["anybody","unlisted","nobody","disable"],"default":"anybody","description":"Vimeo only. Who can view the uploaded video."},"reply_to":{"type":"object","nullable":true,"required":["hash","author_fid"],"properties":{"hash":{"type":"string"},"author_fid":{"type":"integer"}},"description":"Farcaster parent cast. Both the full cast hash and parent author FID are required."},"disable_notification":{"type":"boolean","description":"Telegram only. Sends the channel post silently; subscribers get no notification sound."},"protect_content":{"type":"boolean","description":"Telegram only. Prevents forwarding and saving of the channel post."},"disable_link_preview":{"type":"boolean","description":"Telegram only. Disables the link preview on text posts."}},"description":"Platform-specific settings are mapped explicitly; unknown properties are stored but are not forwarded automatically. For Farcaster, pass { \"channel_id\": \"founders\" } or { \"reply_to\": { \"hash\": \"0x...\", \"author_fid\": 123 } }. To request native AI-content disclosure on TikTok, YouTube, or Instagram, pass { \"ai_generated\": true }. Facebook Page publishing does not currently expose an equivalent API field. For Reddit, pass { \"subreddit\": \"SideProject\", \"title\": \"Launch title\", \"postType\": \"self\" }. For Snapchat, pass { \"placements\": [\"spotlight\", \"public_story\"], \"locale\": \"en_US\" }; placements default to Spotlight. For a TikTok draft upload, pass { \"publish_mode\": \"draft\" }; the creator must finish and publish it from their TikTok inbox. To choose a TikTok cover frame instead of a cover image, pass { \"video_cover_timestamp_ms\": 1500 }. For Vimeo, pass { \"title\": \"Launch video\", \"privacy_view\": \"unlisted\" }; privacy_view defaults to anybody. Telegram accepts only { \"disable_notification\", \"protect_content\", \"disable_link_preview\" } booleans and rejects other keys."}}},"PostTarget":{"type":"object","properties":{"target_post_id":{"type":"string"},"account_id":{"type":"string","nullable":true},"platform":{"type":"string","nullable":true},"target_variant":{"type":"string","enum":["spotlight","public_story"],"nullable":true,"description":"Concrete placement for platforms that fan one account target into multiple publications."},"username":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","scheduled","processing","submitted","published","failed","skipped","cancelled"],"example":"pending"},"error":{"type":"string","nullable":true},"platform_post_id":{"type":"string","nullable":true},"platform_post_url":{"type":"string","nullable":true},"scheduled_time":{"type":"string","format":"date-time","nullable":true}}},"Post":{"type":"object","properties":{"id":{"type":"string"},"content":{"type":"string"},"external_id":{"type":"string","nullable":true},"media":{"type":"array","items":{"$ref":"#/components/schemas/MediaInput"}},"origin":{"type":"string","enum":["ui","api","workflow"],"nullable":true},"publish_at":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","enum":["queued","scheduled","processing","partial","published","failed","cancelled"],"example":"scheduled"},"created_at":{"type":"string","format":"date-time","nullable":true},"targets":{"type":"array","items":{"$ref":"#/components/schemas/PostTarget"}}}},"WorkflowSettings":{"type":"object","properties":{"content_filters":{"type":"object","properties":{"types":{"type":"object","properties":{"text":{"type":"boolean"},"image":{"type":"boolean"},"quote":{"type":"boolean"},"video":{"type":"boolean"}}},"hashtags":{"type":"object","properties":{"include":{"type":"array","items":{"type":"string"}},"exclude":{"type":"array","items":{"type":"string"}}}}}},"platform_settings":{"type":"object","properties":{"tiktok":{"type":"object","properties":{"keep_source_thumbnail":{"type":"boolean","default":true,"description":"When false, TikTok workflow posts do not prepend the source thumbnail as the first video frame."}}}}},"destination_publish_settings":{"type":"object","additionalProperties":{"type":"object","properties":{"minimum_interval_minutes":{"type":"integer","minimum":1},"caption_position":{"type":"string","enum":["before","after"],"description":"Placement of custom caption text. Defaults to after."},"remove_comment_dm_prompts":{"type":"boolean","default":false,"description":"Remove comment/DM prompts from the original caption for this destination before applying custom before/after text. Supported on the same platforms as caption_ending."},"adapt_caption":{"type":"boolean","default":false,"description":"Rewrite the caption for this destination with AI at publish time, after comment/DM prompt removal and before custom text. Links are always kept. If the rewrite fails, the original caption is published. Supported on the same platforms as caption_ending."},"adapt_caption_instructions":{"type":"string","maxLength":1000,"description":"Optional style instructions for the AI rewrite. When set, they replace the built-in guidance for the destination platform."},"caption_ending":{"type":"string","maxLength":2000,"description":"Optional custom caption text for this destination. Placement follows caption_position (after by default). Empty clears it. Supported for Instagram, Threads, X, Facebook, LinkedIn, Bluesky, Pinterest, YouTube and TikTok. Platform caption limits also apply."},"snapchat":{"type":"object","required":["publish_targets"],"properties":{"publish_targets":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","enum":["spotlight","public_story"]}},"locale":{"type":"string","pattern":"^[a-z]{2}_[A-Z]{2}$","default":"en_US"}}},"threads":{"type":"object","properties":{"caption_max_length":{"type":"integer","minimum":1,"maximum":500,"default":500,"description":"Maximum outgoing Threads caption length for this destination. Custom limits prefer whole words; 500 preserves default behavior."}}},"instagram":{"type":"object","anyOf":[{"required":["publish_as_trial_reel"]},{"required":["remove_mentions"]}],"properties":{"remove_mentions":{"type":"boolean","default":false,"description":"Remove @ from mentions in this Instagram destination's outgoing captions. Preserves usernames and email addresses."},"publish_as_trial_reel":{"type":"boolean","default":false,"description":"When true, eligible single-video Instagram Reels publish as Trial Reels for non-followers with manual graduation. Images and carousels publish normally."}}},"reddit":{"type":"object","required":["subreddit"],"properties":{"subreddit":{"type":"string","example":"SideProject"},"post_type":{"type":"string","enum":["auto","self","link"],"default":"auto"},"flair_id":{"type":"string"},"flair_text":{"type":"string"},"nsfw":{"type":"boolean","default":false},"spoiler":{"type":"boolean","default":false},"send_replies":{"type":"boolean","default":true}}}}}}}},"Workflow":{"type":"object","properties":{"id":{"type":"string"},"profile_id":{"type":"string"},"source_account_id":{"type":"string"},"target_account_ids":{"type":"array","items":{"type":"string"}},"settings":{"$ref":"#/components/schemas/WorkflowSettings"},"is_active":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"future_content_enabled":{"type":"boolean"},"last_checked":{"type":"string","format":"date-time","nullable":true},"last_enabled_at":{"type":"string","format":"date-time","nullable":true}}}}},"paths":{"/v1/accounts":{"get":{"summary":"List accounts","description":"Returns all social accounts owned by the API key profile, including disconnected accounts.","responses":{"200":{"description":"Accounts returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Account"}}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/accounts/{id}":{"delete":{"summary":"Disconnect account","description":"Removes PostOnce access to an owned social account. The account is disconnected with its credentials cleared (the response returns deleted=false and disconnected=true); its posts and history are kept, and reconnecting the same account restores posting. Farcaster, Nostr, and Telegram records are also disabled. Farcaster signer revocation requires a separate provider step; Nostr remote-signer logout is best effort; @PostOnceBot leaves a Telegram channel on a best-effort basis when no other PostOnce account uses it. This does not delete published social content.","parameters":[{"$ref":"#/components/parameters/accountId"}],"responses":{"200":{"description":"Account disconnected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"Account not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/accounts/connect/{platform}":{"post":{"summary":"Connect account","description":"Computes a five-minute browser authorization URL bound to the authenticated profile without connecting or changing an account. Open the returned URL in a separate browser step to authorize the provider; then list accounts to verify connection. Depending on platform, the browser uses provider OAuth or a PostOnce-hosted credential or signer flow. Never enter provider credentials in chat.","parameters":[{"$ref":"#/components/parameters/platform"}],"responses":{"201":{"description":"Account connection URL created successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectAccountResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"Missing required scope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unsupported platform.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/media/uploads":{"post":{"summary":"Create upload URL","description":"Creates a signed upload URL and media_id. Upload raw file bytes with PUT to data.upload.signedUrl, then call get_media with that media_id and pass the returned data.public_url as media[].url when creating a post or draft. URL creation alone does not upload bytes. Treat the signed URL as a temporary credential; do not open it in a browser.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","content_type"],"properties":{"filename":{"type":"string","description":"Original file name including extension.","example":"launch-image.png"},"content_type":{"type":"string","description":"MIME type of the file being uploaded.","enum":["image/jpeg","image/png","image/webp","video/mp4","video/quicktime","video/webm"],"example":"image/png"}}},"examples":{"image":{"value":{"filename":"launch-image.png","content_type":"image/png"}},"video":{"value":{"filename":"teaser.mp4","content_type":"video/mp4"}}}}}},"responses":{"201":{"description":"Signed upload URL created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/jpeg","image/png","image/webp","video/mp4","video/quicktime","video/webm"],"example":"image/png"},"media_id":{"type":"string","example":"cHVibGljLWFwaS91c2VyLTEyMy8xNzQzNDM0NTY3LWxhdW5jaC1pbWFnZS5wbmc"},"path":{"type":"string","example":"public-api/user-123/1743434567-launch-image.png"},"upload":{"type":"object","properties":{"path":{"type":"string","example":"public-api/user-123/1743434567-launch-image.png"},"token":{"type":"string","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"signedUrl":{"type":"string","example":"https://swgrcbzlaeshsmygubik.supabase.co/storage/v1/object/upload/sign/social-media/public-api/user-123/1743434567-launch-image.png?token=..."}}}}}}},"examples":{"imageUpload":{"value":{"data":{"content_type":"image/png","media_id":"cHVibGljLWFwaS91c2VyLTEyMy8xNzQzNDM0NTY3LWxhdW5jaC1pbWFnZS5wbmc","path":"public-api/user-123/1743434567-launch-image.png","upload":{"path":"public-api/user-123/1743434567-launch-image.png","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","signedUrl":"https://swgrcbzlaeshsmygubik.supabase.co/storage/v1/object/upload/sign/social-media/public-api/user-123/1743434567-launch-image.png?token=..."}}}}}}}},"400":{"description":"Missing or invalid payload fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidFilename":{"value":{"error":{"code":"invalid_filename","message":"filename is required."}}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/media/{id}":{"get":{"summary":"Get media","description":"Resolves a previously returned `media_id` into its storage path and public URL.","parameters":[{"$ref":"#/components/parameters/mediaId"}],"responses":{"200":{"description":"Media resolved successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Media"}}}}}},"400":{"description":"Invalid media ID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/drafts":{"get":{"summary":"List drafts","description":"Lists unscheduled drafts owned by the authenticated API-key profile.","parameters":[{"$ref":"#/components/parameters/listLimit"},{"$ref":"#/components/parameters/listAfter"},{"name":"after_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Stable ID tie-breaker returned as meta.next_after_id. Send it with after to avoid skipping drafts that share the same updated_at timestamp."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["inbox","ready","archived"]}}],"responses":{"200":{"description":"Drafts returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Draft"}},"meta":{"type":"object","required":["has_more","next_after","next_after_id"],"properties":{"has_more":{"type":"boolean"},"next_after":{"type":"string","format":"date-time","nullable":true},"next_after_id":{"type":"string","format":"uuid","nullable":true}}}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"post":{"summary":"Create draft","description":"Saves content and optional media or destinations without publishing or scheduling it. Reuse an Idempotency-Key when retrying.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"Stable retry key. Reusing it with the same body returns the original draft; reusing it with a different body returns 409."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string"},"candidate_angle":{"type":"string"},"media":{"type":"array","items":{"$ref":"#/components/schemas/DraftMediaInput"}},"selected_account_ids":{"type":"array","uniqueItems":true,"items":{"type":"string","format":"uuid"}},"suggested_platforms":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"201":{"description":"Draft created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Draft"}}}}}},"400":{"description":"Invalid draft or destination account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/drafts/{id}":{"get":{"summary":"Get draft","description":"Returns one draft owned by the authenticated API-key profile.","parameters":[{"$ref":"#/components/parameters/draftId"}],"responses":{"200":{"description":"Draft returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Draft"}}}}}},"404":{"description":"Draft not found for this profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"summary":"Update draft","description":"Updates, archives, restores, or marks a draft ready using updated_at as a compare-and-swap version.","parameters":[{"$ref":"#/components/parameters/draftId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["expected_updated_at"],"properties":{"expected_updated_at":{"type":"string","format":"date-time"},"content":{"type":"string"},"candidate_angle":{"type":"string"},"media":{"type":"array","items":{"$ref":"#/components/schemas/DraftMediaInput"}},"selected_account_ids":{"type":"array","uniqueItems":true,"items":{"type":"string","format":"uuid"}},"suggested_platforms":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["inbox","ready","archived"]}}}}}},"responses":{"200":{"description":"Draft updated successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Draft"}}}}}},"409":{"description":"The draft changed or the requested lifecycle transition is invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/posts":{"get":{"summary":"List posts","description":"Lists the authenticated profile's posts, newest first: posts created through the API or dashboard, and posts published by workflows (origin `workflow`). Workflow posts are read-only.","parameters":[{"$ref":"#/components/parameters/listLimit"},{"$ref":"#/components/parameters/listAfter"}],"responses":{"200":{"description":"Posts returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Post"}},"meta":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"The after cursor is not a post ID or does not match a post.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"post":{"summary":"Create post","description":"Creates one logical post that can target multiple connected accounts. Include `publish_at` to schedule it instead of sending immediately.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["targets"],"properties":{"content":{"type":"string","example":"Shipping the public API today."},"external_id":{"type":"string","nullable":true,"example":"launch-001"},"media":{"type":"array","items":{"$ref":"#/components/schemas/MediaInput"}},"publish_at":{"type":"string","format":"date-time","nullable":true,"example":"2026-04-01T14:00:00.000Z"},"targets":{"type":"array","items":{"$ref":"#/components/schemas/PostTargetInput"}}}},"examples":{"immediate":{"value":{"content":"Shipping the public API today.","external_id":"launch-001","targets":[{"account_id":"account-1"},{"account_id":"account-2","content_override":"Shorter version for X"}]}},"mediaSingle":{"value":{"content":"Launching with a hero image.","media":[{"url":"https://cdn.example.com/launch-image.png","type":"image","width":1200,"height":630}],"targets":[{"account_id":"account-1"}]}},"mediaMultiple":{"value":{"content":"Carousel post with multiple images.","media":[{"url":"https://cdn.example.com/carousel-1.png","type":"image"},{"url":"https://cdn.example.com/carousel-2.png","type":"image"},{"url":"https://cdn.example.com/carousel-3.png","type":"image"}],"targets":[{"account_id":"account-1"}]}},"perTargetCustomization":{"value":{"content":"Shipping the public API today.","targets":[{"account_id":"twitter-account-id","content_override":"Shipping today. API is live."},{"account_id":"linkedin-account-id","platform_options":{"title":"Public API launch"}}]}},"scheduled":{"value":{"content":"Scheduled launch note","publish_at":"2026-04-01T14:00:00.000Z","targets":[{"account_id":"account-1"}]}}}}}},"responses":{"201":{"description":"Post created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"One or more target accounts are invalid or inactive.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/posts/{id}":{"get":{"summary":"Get post","description":"Returns one aggregate post with per-target delivery state.","parameters":[{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Post returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"patch":{"summary":"Update post","description":"Updates a scheduled post before it runs.","parameters":[{"$ref":"#/components/parameters/postId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string"},"external_id":{"type":"string","nullable":true},"media":{"type":"array","items":{"$ref":"#/components/schemas/MediaInput"}},"publish_at":{"type":"string","format":"date-time","nullable":true},"targets":{"type":"array","items":{"$ref":"#/components/schemas/PostTargetInput"}}}},"examples":{"reschedule":{"value":{"publish_at":"2026-04-02T15:00:00.000Z"}},"changeContent":{"value":{"content":"Updated launch copy.","external_id":"launch-002"}}}}}},"responses":{"200":{"description":"Scheduled post updated successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"409":{"description":"Post can no longer be updated in its current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"summary":"Delete post","description":"Cancels a scheduled or still-pending post without deleting its aggregate record or already published social content. This is functionally equivalent to cancel_post.","parameters":[{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Scheduled post cancelled successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/posts/{id}/publish":{"post":{"summary":"Publish now","description":"Starts publishing a scheduled post immediately and returns the aggregate in processing state.","parameters":[{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Publish-now started successfully. The returned post is no longer scheduled.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/posts/{id}/cancel":{"post":{"summary":"Cancel post","description":"Cancels a scheduled or still-pending post without deleting its aggregate record or already published social content.","parameters":[{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Scheduled post cancelled successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}},"/v1/posts/{id}/targets/{targetId}/retry":{"post":{"summary":"Retry failed destination","description":"Retries one failed immediate or scheduled destination without replaying successful, submitted, or published siblings.","parameters":[{"$ref":"#/components/parameters/postId"},{"$ref":"#/components/parameters/targetPostId"}],"responses":{"202":{"description":"Destination retry accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Post"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"409":{"description":"The destination is not failed or is already being retried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/analytics":{"get":{"summary":"Get post analytics","description":"Views, likes, comments, shares and saves for each post the connected TikTok, Instagram, YouTube and Facebook accounts published in the last 7, 30 or 90 days, including posts made outside PostOnce. Each platform copy is compared with its account's median views (`views_vs_account_median`: 2 means twice the account's usual views), so above- and below-average posts can be found and compared. Numbers are cached and refreshed about every 10 minutes; `refreshing` is true while some are still loading. A first report for many accounts can take longer than a few seconds: the call then returns `refreshing: true` with no posts yet and keeps building, so call again in a minute. A metric is null when the platform does not report it. Partner accounts get one end user's posts per call with `external_user_id` or `account_id`, and their own accounts (those without an end user) otherwise. For one post, use `GET /v1/posts/{id}/analytics`.","parameters":[{"name":"days","in":"query","required":false,"description":"Range in days. One of 7, 30 or 90. Defaults to 30.","schema":{"type":"integer","default":30}},{"name":"external_user_id","in":"query","required":false,"description":"Partner accounts only. Report on this end user's accounts. Without it (or account_id), partners get their own accounts.","schema":{"type":"string"}},{"name":"account_id","in":"query","required":false,"description":"Report on one connected account.","schema":{"type":"string"}}],"responses":{"200":{"description":"Analytics returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PostAnalytics"}}}}}},"400":{"description":"days is not 7, 30 or 90.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"account_id is not one of your accounts (or not this end user's).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/posts/{id}/analytics":{"get":{"summary":"Get one post's analytics","description":"Current views, likes, comments, shares and saves for each platform copy of one post that is published on TikTok, Instagram, YouTube or Facebook. Numbers are cached for up to an hour; `last_updated` says when each was read from the platform. Targets on other platforms, or not published yet, are left out. Works for posts created through the API or dashboard and for workflow posts.","parameters":[{"$ref":"#/components/parameters/postId"}],"responses":{"200":{"description":"Post analytics returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PostTargetAnalytics"}}}}}},"400":{"description":"Post ID must be a valid UUID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"404":{"description":"Post not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/workflows":{"get":{"summary":"List workflows","description":"Lists all workflows owned by the authenticated profile.","responses":{"200":{"description":"Workflows returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Workflow"}}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"post":{"summary":"Create a workflow","description":"Creates a future-content cross-post workflow from one source account to one or more target accounts. Use `settings.content_filters` to limit which future posts are eligible.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"A unique key for safely retrying workflow creation. Reuse the same key with the identical request body to receive the original response.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name","source_account_id","target_account_ids"],"properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"source_account_id":{"type":"string"},"target_account_ids":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"future_content_enabled":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/WorkflowSettings"}}},"examples":{"basic":{"value":{"name":"Workflow A to B","source_account_id":"account-1","target_account_ids":["account-2"],"is_active":true}},"withFilters":{"value":{"name":"Facebook to LinkedIn","source_account_id":"account-1","target_account_ids":["account-2","account-3"],"is_active":true,"settings":{"content_filters":{"types":{"text":true,"image":true,"quote":false,"video":true},"hashtags":{"include":[],"exclude":[]}}}}}}}}},"responses":{"201":{"description":"Workflow created successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Workflow"}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}},"403":{"description":"Workflow limit reached for the current plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/workflows/{id}":{"get":{"summary":"Get workflow","description":"Returns one workflow by ID.","parameters":[{"$ref":"#/components/parameters/workflowId"}],"responses":{"200":{"description":"Workflow returned successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Workflow"}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"put":{"summary":"Update a workflow","description":"Updates a future-content workflow configuration, including activation state, target accounts, and optional `settings.content_filters`.","parameters":[{"$ref":"#/components/parameters/workflowId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"description":{"type":"string","nullable":true},"source_account_id":{"type":"string"},"target_account_ids":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"future_content_enabled":{"type":"boolean"},"settings":{"$ref":"#/components/schemas/WorkflowSettings"}}},"examples":{"pauseWorkflow":{"value":{"is_active":false}},"refineFilters":{"value":{"settings":{"content_filters":{"types":{"text":false,"image":true,"quote":false,"video":true},"hashtags":{"include":["launch"],"exclude":["sponsored"]}}}}}}}}},"responses":{"200":{"description":"Workflow updated successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Workflow"}}}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}},"delete":{"summary":"Delete a workflow","description":"Deletes a workflow and any queued workflow items linked to it.","parameters":[{"$ref":"#/components/parameters/workflowId"}],"responses":{"200":{"description":"Workflow deleted successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"401":{"description":"Missing, invalid, revoked, or expired API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthErrorResponse"}}}}}}}}}