{"openapi": "3.1.0", "info": {"title": "Typefully Public API", "version": "2.0.0", "description": "\nThe Typefully Public API allows you to programmatically manage your social media drafts,\nschedule posts, and publish content across multiple platforms.\n\n## Important: X Automation Compliance\n\n**Before building with this API for X automation, please review these critical guidelines:**\n\n- Make sure to adhere to [X automation rules](https://help.x.com/en/rules-and-policies/x-automation) and [general X rules](https://help.x.com/en/rules-and-policies/x-rules) when scheduling content, otherwise your X account might be banned.\n- If you plan to build an app on X that is not just for you or your company to use, you will need to use the [X API with higher rate limits](https://developer.x.com/en) than Typefully's API, which is meant to create personal automations and workflows.\n\n## Authentication\n\nAll requests require a Bearer token in the Authorization header:\n\n```\nAuthorization: Bearer YOUR_API_KEY\n```\n\nGenerate your API key from your Typefully settings.\n\n## Permissions & Access Levels\n\nAPI keys inherit the same permissions as the user who created them. Your access to social sets (accounts)\ndetermines which API operations you can perform.\n\n## Rate Limiting\n\nAPI requests are rate-limited on a per user and per social set basis. When you exceed the rate limit, you'll receive a 429 Too Many Requests response. All API responses include headers showing your current rate limit status:\n\n**User rate limits** (applies to all endpoints, per user): `X-RateLimit-User-Limit` (maximum requests allowed), `X-RateLimit-User-Remaining` (requests remaining), `X-RateLimit-User-Reset` (Unix timestamp when limit resets).\n\n**Social set rate limits** (applies to specific operations like draft creation, per social set): `X-RateLimit-SocialSet-Limit`, `X-RateLimit-SocialSet-Remaining`, `X-RateLimit-SocialSet-Reset`, `X-RateLimit-SocialSet-Resource` (the resource identifier, e.g., \"drafts.create\").\n\n## Pagination\n\nList endpoints use limit-offset pagination for efficient data retrieval:\n\n- **limit**: Maximum items per page (default: 10, max: 50)\n- **offset**: Number of items to skip (default: 0)\n\nExample request:\n```\nGET /v2/social-sets?limit=25&offset=50\n```\n\nEach paginated response includes:\n- **results**: Array of items for the current page\n- **count**: Total number of items available\n- **limit**: Items per page used for this request\n- **offset**: Current offset value\n- **next**: URL for the next page (null if on last page)\n- **previous**: URL for the previous page (null if on first page)\n    ", "contact": {"name": "Typefully API Support", "email": "support@typefully.com"}}, "paths": {"/v2/me": {"get": {"operationId": "typefully_apps_apiv2_handlers_user_get_me", "summary": "Get current user", "parameters": [], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserResponse"}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve the currently authenticated Typefully user associated with your API Key", "tags": ["Users"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets": {"get": {"operationId": "typefully_apps_apiv2_handlers_social_sets_list_social_sets", "summary": "List social sets", "parameters": [{"in": "query", "name": "limit", "schema": {"anyOf": [{"maximum": 50, "minimum": 1, "type": "integer"}, {"type": "null"}], "default": 10, "description": "Maximum number of items to return per page", "title": "Limit"}, "required": false, "description": "Maximum number of items to return per page"}, {"in": "query", "name": "offset", "schema": {"default": 0, "description": "Number of items to skip from the beginning", "minimum": 0, "title": "Offset", "type": "integer"}, "required": false, "description": "Number of items to skip from the beginning"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PagedSocialSetListResponse"}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve all social sets (accounts) you can access. This includes accounts you own directly and accounts that belong to teams you are a member of.", "tags": ["Social Sets"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/": {"get": {"operationId": "typefully_apps_apiv2_handlers_social_sets_get_social_set_details", "summary": "Get social set details", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SocialSetDetailResponse"}}}}, "403": {"description": "You do not have permission to access this social set", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"access_denied": {"summary": "No access to social set", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have access to this social set."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve detailed information about a social set, including every configured social media platform (X, LinkedIn, Mastodon, Threads, Bluesky) with account details and profile information.\n\n**Required permission:** READ access to the social set.", "tags": ["Social Sets"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts": {"get": {"operationId": "typefully_apps_apiv2_handlers_drafts_list_drafts", "summary": "List drafts", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "query", "name": "status", "schema": {"anyOf": [{"enum": ["draft", "published", "scheduled", "planned", "error", "publishing"], "type": "string"}, {"type": "null"}], "description": "Filter by draft status. 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed.", "title": "Status"}, "required": false, "description": "Filter by draft status. 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed."}, {"in": "query", "name": "tag", "schema": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "title": "Tag"}, "required": false}, {"in": "query", "name": "order_by", "schema": {"allOf": [{"description": "Allowed order_by fields for draft listing - prevents SQL injection", "enum": ["created_at", "-created_at", "updated_at", "-updated_at", "scheduled_date", "-scheduled_date", "published_at", "-published_at"], "title": "DraftOrderBy", "type": "string"}], "default": "-updated_at"}, "required": false}, {"in": "query", "name": "limit", "schema": {"anyOf": [{"maximum": 50, "minimum": 1, "type": "integer"}, {"type": "null"}], "default": 10, "description": "Maximum number of items to return per page", "title": "Limit"}, "required": false, "description": "Maximum number of items to return per page"}, {"in": "query", "name": "offset", "schema": {"default": 0, "description": "Number of items to skip from the beginning", "minimum": 0, "title": "Offset", "type": "integer"}, "required": false, "description": "Number of items to skip from the beginning"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PagedDraftListResponse"}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve all drafts for a specific social set with optional filtering and sorting. Drafts are ordered by last edited date (most recent first) by default.\n\n**Draft statuses:** 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed.\n\n**Required permission:** READ access to this social set.", "tags": ["Drafts"], "security": [{"PublicAPIAuthentication": []}]}, "post": {"operationId": "typefully_apps_apiv2_handlers_drafts_create_draft", "summary": "Create draft", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DraftDetailResponse"}}}}, "400": {"description": "Invalid request data or validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"publish_confirmation_required": {"summary": "Immediate publish via MCP without confirm_publish", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Immediate publishing (publish_at=\"now\") requires confirm_publish=true. Ask the user to confirm: show them exactly what will be published and to which platforms, then retry with confirm_publish=true only if they explicitly say yes. Do not change publish_at or schedule the post on your own.", "details": [{"field": "confirm_publish", "message": "Must be true"}]}}}, "no_platforms": {"summary": "No platforms enabled", "value": {"error": {"code": "VALIDATION_ERROR", "message": "At least one platform must be enabled"}}}, "linkedin_validation": {"summary": "LinkedIn validation error", "value": {"error": {"code": "VALIDATION_ERROR", "message": "LinkedIn only supports single posts. Please provide only one post for LinkedIn."}}}, "media_not_found": {"summary": "Media not found", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Media 550e8400-e29b-41d4-a716-446655440000 is still processing. Please wait and try again."}}}, "draft_limit": {"summary": "Draft limit exceeded", "value": {"error": {"code": "VALIDATION_ERROR", "message": "You have reached your draft limit"}}}}}}}, "402": {"description": "Account is paused or requires a paid plan before creating drafts", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"account_paused": {"summary": "Account paused", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "Upgrade your plan to create drafts from this account"}}}}}}}, "403": {"description": "Insufficient permissions or feature not available", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"no_write_access": {"summary": "Insufficient permissions", "value": {"error": {"code": "INSUFFICIENT_ACCESS_LEVEL", "message": "You need write access to create drafts in this social set."}}}, "x_communities_not_available": {"summary": "X communities feature not available", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "You need to upgrade to use X communities"}}}, "linkedin_first_comment_not_available": {"summary": "LinkedIn first comment feature not available", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "You need to upgrade to use LinkedIn first comments"}}}, "x_policy_reply_publish_blocked": {"summary": "Reply publish/schedule blocked by X policy", "value": {"error": {"code": "FORBIDDEN", "message": "This is not allowed by X policy. You can create reply drafts, but publishing or scheduling replies via the API is blocked."}}}, "x_policy_url_direct_publish_blocked": {"summary": "URL direct publish blocked by X policy", "value": {"error": {"code": "FORBIDDEN", "message": "This is not allowed by X policy. Direct publishing of X drafts containing URLs is blocked."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Create a new draft with content for one or more social media platforms. The draft can be saved as a draft, planned (`plan_at`: dated but inert - it never auto-publishes until confirmed), scheduled for later publishing, or published immediately.\n\n**Account-level settings:** This endpoint automatically applies the following account-level settings if enabled: Auto-Retweet, Auto-Plug, and Natural Posting Time.\n\n**Required permission:** WRITE access to create drafts (planning included - it creates no publishing commitment). PUBLISH access is required to schedule or publish immediately.", "tags": ["Drafts"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DraftCreateRequest"}, "examples": {"Multi-platform draft": {"summary": "Multi-platform draft", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "1/ Excited to share our latest product update!"}, {"text": "2/ We've added new features based on your feedback."}, {"text": "3/ Try it out and let us know what you think!"}]}, "linkedin": {"enabled": true, "posts": [{"text": "Excited to share our latest product update! We've added new features based on your feedback. Try it out and let us know what you think!"}]}}, "share": true}}, "X post": {"summary": "X post", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Hello world! This is my first post."}]}}}}, "Planned draft": {"summary": "Planned draft (dated but inert)", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Sketching next week's announcement."}]}}, "plan_at": "2027-01-20T14:00:00Z"}}, "X quote post": {"summary": "X quote post", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "My thoughts on this", "quote_post_url": "https://x.com/typefully/status/2025894220243063023"}]}}}}, "X post with content disclosures": {"summary": "X post with content disclosures", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Sponsored post made with AI visuals", "paid_partnership": true, "made_with_ai": true}]}}}}, "X post to community": {"summary": "X post to community", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Hello community! Sharing this with everyone."}], "settings": {"community_id": "1493446837214187523"}}}}}, "X reply": {"summary": "X reply", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "This is a reply to another post!"}], "settings": {"reply_to_url": "https://x.com/username/status/1234567890"}}}}}, "X Article": {"summary": "X Article", "value": {"platforms": {"x_article": {"content_markdown": "# Think Different, Draft Different\n\nGreat drafts start when builders **question defaults**, *shape the rough edges*, and ~~wait for perfect certainty~~ publish what helps.\n\n> The best interface is the one readers forget they are using.\n\n# Working notes\n\n- Start with a sharp title\n- Use structure before decoration\n- Link only when [context helps](https://typefully.com)\n\n## Final pass\n\n1. Cut filler\n2. Keep the useful tension\n3. Ship the clearer version\n\n```python\nprint(\"Ship it\")\n```"}}}}, "X Article with embeds": {"summary": "X Article with embeds", "value": {"platforms": {"x_article": {"content_markdown": "# My article title\n\nIntro paragraph.\n\n## Section\n\nBody text.\n\n<typ:media media_id=\"550e8400-e29b-41d4-a716-446655440000\" />\n\n<typ:x-post url=\"https://x.com/typefully/status/2025894220243063023\" />", "cover_media_id": "550e8400-e29b-41d4-a716-446655440000"}}}}, "LinkedIn post": {"summary": "LinkedIn post", "value": {"platforms": {"linkedin": {"enabled": true, "posts": [{"text": "Sharing some thoughts on building great products."}]}}}}, "LinkedIn post with mention": {"summary": "LinkedIn post with mention", "value": {"platforms": {"linkedin": {"enabled": true, "posts": [{"text": "Thanks @[Typefully](urn:li:organization:86779668) for helping us ship faster."}]}}}}}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}": {"get": {"operationId": "typefully_apps_apiv2_handlers_drafts_get_draft", "summary": "Get draft", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "query", "name": "exclude_comment_markers", "schema": {"default": false, "description": "When true, render `posts[*].text` as plain user-visible text without `<typ:comment-thread>` markers, and render X Article `content_markdown` without comment markers. Use only for read-only flows (LLM context windows, exports). The default (false) emits markers so a round-trip back to PATCH preserves comment anchors.", "title": "Exclude Comment Markers", "type": "boolean"}, "required": false, "description": "When true, render `posts[*].text` as plain user-visible text without `<typ:comment-thread>` markers, and render X Article `content_markdown` without comment markers. Use only for read-only flows (LLM context windows, exports). The default (false) emits markers so a round-trip back to PATCH preserves comment anchors."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DraftDetailResponse"}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve a specific draft by ID, including its content for all configured platforms, status, and scheduling information.\n\nIf the draft has comment threads, the response includes Typefully comment-thread markers in `posts[*].text` and, for X Articles, in `platforms.x_article.content_markdown`. These markers are structural anchor metadata for `GET \u2192 modify \u2192 PATCH` round-trips; preserve them exactly when editing.\n\nFor read-only display/export, pass `?exclude_comment_markers=true` to render draft text without markers. Content returned with that flag set should not be PATCHed back unless you intend to resolve or remove comment anchors.\n\n**Draft statuses:** 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed.\n\n**Required permission:** READ access to this social set.", "tags": ["Drafts"], "security": [{"PublicAPIAuthentication": []}]}, "patch": {"operationId": "typefully_apps_apiv2_handlers_drafts_edit_draft", "summary": "Update draft", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "query", "name": "exclude_comment_markers", "schema": {"default": false, "description": "Render the response's `posts[*].text` as plain text without `<typ:comment-thread>` markers, and render X Article `content_markdown` without comment markers. Render-only \u2014 does not affect request-body validation.", "title": "Exclude Comment Markers", "type": "boolean"}, "required": false, "description": "Render the response's `posts[*].text` as plain text without `<typ:comment-thread>` markers, and render X Article `content_markdown` without comment markers. Render-only \u2014 does not affect request-body validation."}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DraftDetailResponse"}}}}, "400": {"description": "Invalid request data or draft cannot be edited (e.g., already published)", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"already_published": {"summary": "Draft already published", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Cannot edit a draft that has already been published"}}}, "publish_confirmation_required": {"summary": "Immediate publish via MCP without confirm_publish", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Immediate publishing (publish_at=\"now\") requires confirm_publish=true. Ask the user to confirm: show them exactly what will be published and to which platforms, then retry with confirm_publish=true only if they explicitly say yes. Do not change publish_at or schedule the post on your own.", "details": [{"field": "confirm_publish", "message": "Must be true"}]}}}, "no_platforms": {"summary": "No platforms enabled", "value": {"error": {"code": "VALIDATION_ERROR", "message": "At least one platform must be enabled"}}}}}}}, "402": {"description": "Account is paused or requires a paid plan before editing drafts", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"account_paused": {"summary": "Account paused", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "Resume your subscription to edit drafts from this account"}}}}}}}, "403": {"description": "Insufficient permissions or feature not available", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"insufficient_access": {"summary": "Missing publish permission", "value": {"error": {"code": "INSUFFICIENT_ACCESS_LEVEL", "message": "You need publish access to edit scheduled drafts in this social set."}}}, "x_communities_not_available": {"summary": "X communities feature not available", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "You need to upgrade to use X communities"}}}, "linkedin_first_comment_not_available": {"summary": "LinkedIn first comment feature not available", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "You need to upgrade to use LinkedIn first comments"}}}, "x_policy_reply_publish_blocked": {"summary": "Reply publish/schedule blocked by X policy", "value": {"error": {"code": "FORBIDDEN", "message": "This is not allowed by X policy. You can create reply drafts, but publishing or scheduling replies via the API is blocked."}}}, "x_policy_url_direct_publish_blocked": {"summary": "URL direct publish blocked by X policy", "value": {"error": {"code": "FORBIDDEN", "message": "This is not allowed by X policy. Direct publishing of X drafts containing URLs is blocked."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Update an existing draft with partial update semantics. Only provided fields are updated; omitted fields remain unchanged. Scheduled drafts require publish access to edit.\n\n## Planning\n\nA planned draft is dated but inert: it has a `scheduled_date` but never auto-publishes until confirmed. Use `plan_at` and `publish_at` (mutually exclusive per request) to move a draft through the lifecycle:\n\n- `plan_at=<datetime|\"next-free-slot\">` on a plain draft plans it, on a planned draft moves the date (both write access), and on a scheduled draft unschedules it into a plan (publish access - it disarms a live schedule).\n- `publish_at=<datetime|\"next-free-slot\">` on a planned draft confirms it into a real schedule (publish access; echo the draft's `scheduled_date` to confirm at the planned date). `publish_at=\"now\"` publishes it immediately.\n- An explicit `plan_at=null` or `publish_at=null` clears the date and returns the draft to plain draft status (write access from planned, publish access from scheduled).\n- Content edits on a planned draft require write access only.\n\n## Note about Comment-thread markers\n\nIf the draft has comment threads, submitted `posts[*].text` and X Article `platforms.x_article.content_markdown` must preserve the Typefully comment-thread markers received from `GET /drafts/{id}`. Validation is platform-level: every comment thread anchored on a platform must appear somewhere in that platform's submitted text.\n\nRecommended edit flow: GET the draft without `exclude_comment_markers`, modify text while preserving markers exactly, then PATCH with `force_overwrite_comments: false` (the default).\n\n- `409 COMMENTS_MARKER_MISMATCH` will be thrown if an expected comment thread marker is missing unless `\"force_overwrite_comments\": true` is set, in which case the affected threads are resolved server-side.\n- `400 COMMENTS_MARKER_UNKNOWN_ID` will be thrown if you submit an id that doesn't exist on this draft. - `400 COMMENTS_MARKER_MALFORMED` will be thrown if the marker tag is malformed (bad UUID, unbalanced, attribute violations, etc.).\n\nPass `?exclude_comment_markers=true` to render the response text without markers (read-only / display rendering \u2014 does NOT skip server-side marker validation on the request body). Do not PATCH content returned with that flag unless you intend to resolve or remove comment anchors.\n\n**Required permission:** WRITE access to edit drafts. PUBLISH access is required to edit scheduled drafts, schedule, or publish.", "tags": ["Drafts"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DraftUpdateRequest"}, "examples": {"update_text": {"summary": "Update post text", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Updated post content!"}]}}}}, "update_x_quote_post": {"summary": "Update X quote post", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Adding context to this post", "quote_post_url": "https://x.com/typefully/status/2025894220243063023"}]}}}}, "update_x_content_disclosures": {"summary": "Update X content disclosures", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Sponsored AI-assisted post", "paid_partnership": true, "made_with_ai": true}]}}}}, "update_x_reply_settings": {"summary": "Limit who can reply to an X post", "value": {"platforms": {"x": {"enabled": true, "posts": [{"text": "Only accounts I follow can reply to this", "reply_settings": "following"}]}}}}, "share_draft": {"summary": "Enable draft sharing", "value": {"share": true}}, "schedule": {"summary": "Schedule draft", "value": {"publish_at": "2027-01-20T14:00:00Z"}}, "plan": {"summary": "Plan draft (dated but inert)", "value": {"plan_at": "2027-01-20T14:00:00Z"}}, "confirm_plan": {"summary": "Confirm a planned draft (echo its scheduled_date)", "value": {"publish_at": "2027-01-20T14:00:00Z"}}, "move_to_draft": {"summary": "Clear the date (planned/scheduled back to draft)", "value": {"plan_at": null}}, "update_tags": {"summary": "Update tags", "value": {"tags": ["marketing", "product-launch"]}}, "disable_platform": {"summary": "Disable X platform", "value": {"platforms": {"x": {"enabled": false}}}}, "update_x_article": {"summary": "Replace X Article content", "value": {"platforms": {"x_article": {"content_markdown": "# Updated article title\n\nUpdated body text.", "cover_media_id": null}}}}}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}, "delete": {"operationId": "typefully_apps_apiv2_handlers_drafts_delete_draft", "summary": "Delete draft", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}], "responses": {"204": {"description": "No Content"}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Delete a draft. Requires WRITE access. You can delete your own drafts in any status (DRAFT, ERROR, SCHEDULED, PUBLISHED, PUBLISHING) with WRITE access. Drafts created by other users also require WRITE access to delete.\n\n**Required permission:** WRITE access to this social set.", "tags": ["Drafts"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/media/upload": {"post": {"operationId": "typefully_apps_apiv2_handlers_media_create_media_upload", "summary": "Create media upload", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"201": {"description": "Upload URL created successfully", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MediaUploadResponse"}, "example": {"media_id": "550e8400-e29b-41d4-a716-446655440000", "upload_url": "https://s3.amazonaws.com/bucket/path/file.jpg?X-Amz-Algorithm=..."}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Generate a presigned S3 upload URL for images, videos, GIFs, or PDFs. After you receive the URL, upload the file contents with a PUT request and then reference the returned media_id when creating drafts.\n\n**Uploading:** Send a plain PUT with only raw file bytes as the body \u2014 no extra headers (`Content-Type`, `Authorization`, etc.). The presigned URL signature was calculated without them, so adding headers causes a `403 SignatureDoesNotMatch`. Use `curl -T <file>` (not `--data-binary`), `requests.put(url, data=file_bytes)` in Python, or `fetch(url, {method:'PUT', body:buffer})` in JS. A successful upload returns `200` or `204`.\n\n**Required permission:** WRITE access to the social set.", "tags": ["Media"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MediaUploadRequest"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/media/{media_id}": {"get": {"operationId": "typefully_apps_apiv2_handlers_media_get_media_status", "summary": "Get media status", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "media_id", "schema": {"format": "uuid", "title": "Media Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MediaStatusResponse"}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieves the processing status of an uploaded media file. Poll this endpoint after uploading to check when the file is ready to use in drafts.\n\nIf no file is received before the upload URL expires (1 hour), the media transitions to 'failed' \u2014 create a new media upload and try again.\n\n**Required permission:** READ access to the social set.", "tags": ["Media"], "security": [{"PublicAPIAuthentication": []}]}, "patch": {"operationId": "typefully_apps_apiv2_handlers_media_update_media", "summary": "Update media", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "media_id", "schema": {"format": "uuid", "title": "Media Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MediaStatusResponse"}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Updates metadata of an uploaded media file. Currently supports setting or clearing `alt_text`.\n\nAlt text is copied into a draft when the media is attached (draft create or edit), so set it **before** referencing the media in a draft. To reliably change alt text on media already attached to a draft, update the media and then re-send the draft content with a draft PATCH. (Some platforms fall back to the media's current alt text when the draft snapshot has none, but only re-sending the draft content updates it everywhere.)\n\n**Required permission:** WRITE access to the social set.", "tags": ["Media"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/MediaUpdateRequest"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/tags": {"get": {"operationId": "typefully_apps_apiv2_handlers_tags_list_tags", "summary": "List tags", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "query", "name": "limit", "schema": {"anyOf": [{"maximum": 50, "minimum": 1, "type": "integer"}, {"type": "null"}], "default": 10, "description": "Maximum number of items to return per page", "title": "Limit"}, "required": false, "description": "Maximum number of items to return per page"}, {"in": "query", "name": "offset", "schema": {"default": 0, "description": "Number of items to skip from the beginning", "minimum": 0, "title": "Offset", "type": "integer"}, "required": false, "description": "Number of items to skip from the beginning"}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PagedTagResponse"}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve all tags for a social set, ordered by their slugs.\n\n**Required permission:** READ access to the social set.", "tags": ["Tags"], "security": [{"PublicAPIAuthentication": []}]}, "post": {"operationId": "typefully_apps_apiv2_handlers_tags_create_tag", "summary": "Create tag", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TagResponse"}}}}, "400": {"description": "Invalid request data or validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"duplicate_tag": {"summary": "Duplicate tag name", "value": {"error": {"code": "VALIDATION_ERROR", "message": "You already have a tag with this name"}}}, "empty_slug": {"summary": "Tag name too short", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Tag name is too short"}}}}}}}, "403": {"description": "Insufficient permissions or feature not available", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"no_manage_access": {"summary": "Insufficient permissions", "value": {"error": {"code": "INSUFFICIENT_ACCESS_LEVEL", "message": "You need write access to create tags in this social set."}}}, "feature_not_enabled": {"summary": "Tags feature not enabled", "value": {"error": {"code": "MONETIZATION_ERROR", "message": "You need to upgrade to create tags"}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Create a new tag for a social set. The slug is automatically generated from the tag name, which must be unique per social set.\n\n**Required permission:** WRITE access to the social set.", "tags": ["Tags"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/TagCreateRequest"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/queue/schedule": {"get": {"operationId": "typefully_apps_apiv2_handlers_queue_get_queue_schedule", "summary": "Get queue schedule", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "Queue schedule rules for this social set.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueueScheduleResponse"}, "examples": {"default_schedule": {"summary": "Default schedule example", "value": {"social_set_id": 123, "timezone": "America/New_York", "rules": [{"h": 12, "m": 0, "days": ["mon", "tue", "wed", "thu", "fri"]}, {"h": 17, "m": 0, "days": ["mon", "tue", "wed", "thu", "fri"]}]}}}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve the queue schedule rules for a social set.\n\n**Required permission:** READ access to this social set.\n\nBehavior:\n- If the schedule row does not exist yet, it is created with defaults.", "tags": ["Queue"], "security": [{"PublicAPIAuthentication": []}]}, "put": {"operationId": "typefully_apps_apiv2_handlers_queue_put_queue_schedule", "summary": "Replace queue schedule", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}], "responses": {"200": {"description": "Updated queue schedule rules.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueueScheduleResponse"}, "examples": {"updated_schedule": {"summary": "Updated schedule example", "value": {"social_set_id": 123, "timezone": "America/New_York", "rules": [{"h": 9, "m": 30, "days": ["mon", "wed", "fri"]}]}}}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Replace the queue schedule rules for a social set.\n\n**Required permission:** ADMIN access to this social set.\nSemantics: full replacement (atomic).\n\nRule validation:\n- `h` in `0..23`, `m` in `0..59`\n- `days` values are one of: `mon,tue,wed,thu,fri,sat,sun`\n- Duplicate day+time combinations are rejected\n\nNote: `rules=[]` is allowed and represents an empty schedule.", "tags": ["Queue"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueueScheduleUpdateRequest"}, "examples": {"replace_rules": {"summary": "Replace schedule rules", "value": {"rules": [{"h": 9, "m": 30, "days": ["mon", "wed", "fri"]}]}}, "empty_rules": {"summary": "Empty schedule (no slots)", "value": {"rules": []}}}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/queue": {"get": {"operationId": "typefully_apps_apiv2_handlers_queue_get_queue", "summary": "Get queue", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "query", "name": "start_date", "schema": {"title": "Start Date", "type": "string"}, "required": true}, {"in": "query", "name": "end_date", "schema": {"title": "End Date", "type": "string"}, "required": true}], "responses": {"200": {"description": "Queue view (slots + scheduled drafts) for a date range.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QueueResponse"}, "examples": {"queue": {"summary": "Queue example", "value": {"social_set_id": 123, "start_date": "2026-02-01", "end_date": "2026-02-29", "days": [{"date": "2026-02-12", "items": [{"at": "2026-02-12T17:00:00Z", "kind": "queue_slot", "draft": null}, {"at": "2026-02-12T22:00:00Z", "kind": "queue_slot", "draft": {"id": 98765, "preview": "Hello world", "scheduled_date": "2026-02-12T22:00:00Z", "draft_title": null, "mastodon_post_enabled": false, "social_set_id": 123, "share_url": null, "private_url": "https://typefully.com/?d=98765&a=123", "status": "scheduled", "tags": [], "created_at": "2026-02-12T21:30:00Z", "updated_at": null, "published_at": null, "mastodon_post_published_at": null, "linkedin_post_published_at": null, "threads_post_published_at": null, "bluesky_post_published_at": null, "substack_post_published_at": null, "x_post_published_at": null, "x_post_enabled": true, "linkedin_post_enabled": false, "threads_post_enabled": false, "bluesky_post_enabled": false, "substack_post_enabled": false, "x_published_url": null, "linkedin_published_url": null, "mastodon_published_url": null, "threads_published_url": null, "bluesky_published_url": null, "substack_published_url": null}}, {"at": "2026-02-12T22:00:00Z", "kind": "custom_time", "draft": {"id": 99999, "preview": "Hello world", "scheduled_date": "2026-02-12T22:00:00Z", "draft_title": null, "mastodon_post_enabled": false, "social_set_id": 123, "share_url": null, "private_url": "https://typefully.com/?d=98765&a=123", "status": "scheduled", "tags": [], "created_at": "2026-02-12T21:30:00Z", "updated_at": null, "published_at": null, "mastodon_post_published_at": null, "linkedin_post_published_at": null, "threads_post_published_at": null, "bluesky_post_published_at": null, "substack_post_published_at": null, "x_post_published_at": null, "x_post_enabled": true, "linkedin_post_enabled": false, "threads_post_enabled": false, "bluesky_post_enabled": false, "substack_post_enabled": false, "x_published_url": null, "linkedin_published_url": null, "mastodon_published_url": null, "threads_published_url": null, "bluesky_published_url": null, "substack_published_url": null}}]}]}}}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve queue slots and the scheduled AND planned drafts occupying them, between `start_date` and `end_date` (inclusive). Check each draft's `status`: 'scheduled' = will auto-publish at its `scheduled_date`; 'planned' = dated but inert - it will NOT auto-publish until confirmed (a past planned date is not overdue and not a failure).\n\n**Required permission:** READ access to this social set.\n\nNotes:\n- `start_date` and `end_date` are interpreted in the social set timezone.\n- Ranges larger than 62 days are rejected.", "tags": ["Queue"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/linkedin/organizations/resolve": {"get": {"operationId": "typefully_apps_apiv2_handlers_linkedin_resolve_linkedin_organization_from_url", "summary": "Resolve LinkedIn organization from URL", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "query", "name": "organization_url", "schema": {"description": "Public LinkedIn organization URL (company or school profile).", "examples": ["https://www.linkedin.com/company/typefullycom", "https://www.linkedin.com/school/harvard-university/"], "title": "Organization Url", "type": "string"}, "required": true, "description": "Public LinkedIn organization URL (company or school profile).", "examples": {"example1": {"value": "https://www.linkedin.com/company/typefullycom"}, "example2": {"value": "https://www.linkedin.com/school/harvard-university/"}}}], "responses": {"200": {"description": "Resolved LinkedIn organization metadata", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LinkedInOrganizationFromURLResponse"}, "examples": {"organization_resolved": {"summary": "LinkedIn organization resolved from URL", "value": {"id": "86779668", "urn": "urn:li:organization:86779668", "mention_text": "@[Typefully](urn:li:organization:86779668)", "name": "Typefully", "vanity_name": "typefullycom", "description": "Social media scheduling platform", "website": "https://typefully.com", "logo_url": "https://media.licdn.com/dms/image/....png", "url": "https://www.linkedin.com/company/typefullycom"}}}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}, "503": {"description": "Service unavailable", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"service_unavailable": {"summary": "Temporary outage", "value": {"error": {"code": "SERVICE_UNAVAILABLE", "message": "Service unavailable. Please retry later."}}}}}}}}, "description": "Resolve a LinkedIn company/school URL into organization metadata that can be used to build LinkedIn mention syntax in post text.\n\nThis endpoint is resolver-only and is not a general organization search endpoint.\n\nMention format: `@[Company Name](urn:li:organization:123456)`\n\n**Required permission:** READ access to the social set.", "tags": ["Social Sets"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads": {"get": {"operationId": "typefully_apps_apiv2_handlers_comments_list_comments", "summary": "List comment threads on a draft", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "query", "name": "platform", "schema": {"anyOf": [{"enum": ["x", "linkedin", "mastodon", "threads", "bluesky", "substack", "x_article"], "type": "string"}, {"type": "null"}], "description": "Optional platform filter.", "title": "Platform"}, "required": false, "description": "Optional platform filter."}, {"in": "query", "name": "status", "schema": {"default": "unresolved", "description": "Resolution filter. Defaults to `unresolved`, so resolved threads are omitted unless you request `resolved` or `all`.", "enum": ["unresolved", "resolved", "all"], "title": "Status", "type": "string"}, "required": false, "description": "Resolution filter. Defaults to `unresolved`, so resolved threads are omitted unless you request `resolved` or `all`."}, {"in": "query", "name": "limit", "schema": {"default": 10, "maximum": 50, "minimum": 1, "title": "Limit", "type": "integer"}, "required": false}, {"in": "query", "name": "offset", "schema": {"default": 0, "minimum": 0, "title": "Offset", "type": "integer"}, "required": false}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentThreadListResponse"}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Retrieve comment threads attached to a draft, ordered by creation time. Each thread includes the original `selected_text` snapshot and the full ordered list of comments.\n\nBy default this endpoint returns only unresolved threads (`status=unresolved`). Use `status=resolved` to list resolved threads or `status=all` to list both unresolved and resolved threads. Example: `GET /v2/social-sets/4/drafts/12/comment-threads?status=all&limit=50`.\n\n**Required permission:** READ access to this social set.", "tags": ["Comments"], "security": [{"PublicAPIAuthentication": []}]}, "post": {"operationId": "typefully_apps_apiv2_handlers_comments_create_comment", "summary": "Create a comment thread", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentThreadResponse"}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Create a new comment thread on a draft. A comment thread is the anchored discussion container; comments are the messages inside the thread.\n\nFor post platforms (`x`, `linkedin`, `mastodon`, `threads`, `bluesky`, `substack`), provide `post_index` and anchor to a substring of that post's visible text using `selected_text`. Use `occurrence` to disambiguate when the same substring appears more than once; omit it to anchor on the first match.\n\nFor X Articles, send `platform: \"x_article\"` and omit `post_index` (or use 0). `selected_text` matches commentable article text in document order; Markdown syntax, media tags, X post embed tags, and fenced-code text are ignored. Code blocks do not support comment anchors. X Article text comments cannot overlap existing stored X Article anchors; unsupported or overlapping selections return `400 VALIDATION_ERROR`.\n\nLinkedIn mentions appear in `posts[*].text` as `@[Name](urn:li:organization:ID)` or `@[Name](urn:li:person:ID)`. A mention is indivisible \u2014 `selected_text` must either contain the entire mention substring or stay outside it.\n\n**Required permission:** WRITE access to this social set.", "tags": ["Comments"], "requestBody": {"content": {"application/json": {"schema": {"anyOf": [{"$ref": "#/components/schemas/CommentThreadCreateRequest"}, {"$ref": "#/components/schemas/XArticleCommentThreadCreateRequest"}], "title": "Payload"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads/{comment_thread_id}/comments": {"post": {"operationId": "typefully_apps_apiv2_handlers_comments_add_comment_to_thread", "summary": "Add a comment to an existing comment thread", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "path", "name": "comment_thread_id", "schema": {"format": "uuid", "title": "Comment Thread Id", "type": "string"}, "required": true}], "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentThreadResponse"}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Append a comment to an existing comment thread. Returns the full updated thread.\n\n**Required permission:** WRITE access to this social set.", "tags": ["Comments"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentCreateRequest"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads/{comment_thread_id}/resolve": {"post": {"operationId": "typefully_apps_apiv2_handlers_comments_resolve_thread", "summary": "Resolve a comment thread", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "path", "name": "comment_thread_id", "schema": {"format": "uuid", "title": "Comment Thread Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentThreadResponse"}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Resolves the comment thread and removes the corresponding comment markers from the text.\n\n**Required permission:** Either authorship of the comment thread or WRITE access on the social set.", "tags": ["Comments"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads/{comment_thread_id}/comments/{comment_id}": {"patch": {"operationId": "typefully_apps_apiv2_handlers_comments_update_comment", "summary": "Update a comment's text", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "path", "name": "comment_thread_id", "schema": {"format": "uuid", "title": "Comment Thread Id", "type": "string"}, "required": true}, {"in": "path", "name": "comment_id", "schema": {"format": "uuid", "title": "Comment Id", "type": "string"}, "required": true}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentThreadResponse"}}}}, "400": {"description": "Request failed validation", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"validation_error": {"summary": "Invalid request", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Validation failed.", "details": [{"field": "draft_title", "message": "This field is required."}]}}}}}}}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "422": {"description": "Schema validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"schema_validation": {"summary": "Invalid payload", "value": {"error": {"code": "VALIDATION_ERROR", "message": "Some fields are invalid.", "details": [{"field": "platforms.x.posts.0.text", "message": "Must not be empty."}]}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Update the plain-text body of a single comment. Returns the full updated thread.\n\n**Required permission:** Authorship of the comment.", "tags": ["Comments"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/CommentUpdateRequest"}}}, "required": true}, "security": [{"PublicAPIAuthentication": []}]}, "delete": {"operationId": "typefully_apps_apiv2_handlers_comments_delete_comment", "summary": "Delete a comment", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "path", "name": "comment_thread_id", "schema": {"format": "uuid", "title": "Comment Thread Id", "type": "string"}, "required": true}, {"in": "path", "name": "comment_id", "schema": {"format": "uuid", "title": "Comment Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Deletes a comment from a comment thread. If `comment_id` identifies the *root* (oldest) comment, the entire thread is deleted and the corresponding comment markers are removed from the text.\n\n**Required permission:** Authorship of the comment or WRITE access on the social set.", "tags": ["Comments"], "security": [{"PublicAPIAuthentication": []}]}}, "/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads/{comment_thread_id}": {"delete": {"operationId": "typefully_apps_apiv2_handlers_comments_delete_thread", "summary": "Delete a comment thread", "parameters": [{"in": "path", "name": "social_set_id", "schema": {"title": "Social Set Id", "type": "integer"}, "required": true}, {"in": "path", "name": "draft_id", "schema": {"title": "Draft Id", "type": "integer"}, "required": true}, {"in": "path", "name": "comment_thread_id", "schema": {"format": "uuid", "title": "Comment Thread Id", "type": "string"}, "required": true}], "responses": {"204": {"description": "No Content"}, "401": {"description": "Missing or invalid authentication", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"unauthorized": {"summary": "Invalid API key", "value": {"error": {"code": "UNAUTHORIZED", "message": "Invalid or missing API key."}}}}}}}, "403": {"description": "Insufficient permissions", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"forbidden": {"summary": "Not allowed", "value": {"error": {"code": "FORBIDDEN", "message": "You do not have permission to perform this action."}}}}}}}, "404": {"description": "Resource not found", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"not_found": {"summary": "Missing resource", "value": {"error": {"code": "NOT_FOUND", "message": "Resource not found."}}}}}}}, "429": {"description": "Rate limited", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/APIErrorResponse"}, "examples": {"rate_limited": {"summary": "Too many requests", "value": {"error": {"code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again later."}}}}}}}}, "description": "Deletes the comment thread along with all its comments and removes the corresponding comment markers from the text.\n\n**Required permission:** Authorship of the comment thread or WRITE access on the social set.", "tags": ["Comments"], "security": [{"PublicAPIAuthentication": []}]}}}, "components": {"schemas": {"UserResponse": {"description": "User details response schema", "properties": {"id": {"description": "Unique identifier for the user", "examples": [12345, 67890], "title": "Id", "type": "integer"}, "name": {"description": "Name of the user", "examples": ["John Doe", "Jane Smith"], "title": "Name", "type": "string"}, "email": {"description": "Email address of the user", "examples": ["user@example.com"], "title": "Email", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the user's profile image. Null if not available.", "examples": ["https://example.com/avatar.jpg"], "title": "Profile Image Url"}, "signup_date": {"description": "Timestamp when the user signed up (ISO 8601 format in UTC)", "examples": ["2024-01-15T10:30:00Z"], "format": "date-time", "title": "Signup Date", "type": "string"}, "api_key_label": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Label of the API key used to authenticate this request. Null if no label was set.", "examples": ["My Production Key", "Zapier Integration"], "title": "Api Key Label"}}, "required": ["id", "name", "email", "signup_date"], "title": "UserResponse", "type": "object"}, "APIErrorResponse": {"description": "Standard error response for all API errors.", "properties": {"error": {"$ref": "#/components/schemas/ErrorObject"}}, "required": ["error"], "title": "APIErrorResponse", "type": "object"}, "ErrorDetail": {"description": "Field-level error detail.", "properties": {"message": {"description": "Human-readable error message", "title": "Message", "type": "string"}, "field": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Field name that caused the error", "title": "Field"}, "type": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Error type (e.g. 'string_too_long', 'list_max_items')", "title": "Type"}, "loc": {"anyOf": [{"items": {}, "type": "array"}, {"type": "null"}], "description": "Error location path (e.g. ['body', 'platforms', 'x', 'posts', 0, 'text'])", "title": "Loc"}, "ctx": {"anyOf": [{"additionalProperties": true, "type": "object"}, {"type": "null"}], "description": "Error context", "title": "Ctx"}, "input": {"anyOf": [{}, {"type": "null"}], "description": "Input value that caused the error (e.g. 'Hello, world!')", "title": "Input"}}, "required": ["message"], "title": "ErrorDetail", "type": "object"}, "ErrorObject": {"description": "Standard error response object.", "properties": {"code": {"description": "Error code", "title": "Code", "type": "string"}, "message": {"description": "Human-readable error message", "title": "Message", "type": "string"}, "details": {"anyOf": [{"items": {"$ref": "#/components/schemas/ErrorDetail"}, "type": "array"}, {"type": "null"}], "description": "List of field-level errors (optional - only present when applicable)", "title": "Details"}}, "required": ["code", "message"], "title": "ErrorObject", "type": "object"}, "Input": {"description": "Pagination input parameters.", "properties": {"limit": {"anyOf": [{"maximum": 50, "minimum": 1, "type": "integer"}, {"type": "null"}], "default": 10, "description": "Maximum number of items to return per page", "title": "Limit"}, "offset": {"default": 0, "description": "Number of items to skip from the beginning", "minimum": 0, "title": "Offset", "type": "integer"}}, "title": "Input", "type": "object"}, "PagedSocialSetListResponse": {"properties": {"results": {"items": {"$ref": "#/components/schemas/SocialSetListResponse"}, "title": "Results", "type": "array"}, "count": {"title": "Count", "type": "integer"}, "limit": {"title": "Limit", "type": "integer"}, "offset": {"title": "Offset", "type": "integer"}, "next": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next"}, "previous": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Previous"}}, "required": ["results", "count", "limit", "offset", "next", "previous"], "title": "PagedSocialSetListResponse", "type": "object"}, "SocialSetListResponse": {"description": "Details of the social set for list endpoints", "properties": {"id": {"description": "Unique identifier for the social set", "examples": [12345, 67890], "title": "Id", "type": "integer"}, "username": {"description": "Username/handle for the social media account", "examples": ["elonmusk", "typefully"], "title": "Username", "type": "string"}, "name": {"description": "Display name of the social media account", "examples": ["Elon Musk", "Typefully"], "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image of the social media account. Null if not available.", "examples": ["https://typefully-user-avatars.s3.amazonaws.com/_generic/account/537/twitter.jpeg"], "title": "Profile Image Url"}, "team": {"anyOf": [{"$ref": "#/components/schemas/Team"}, {"type": "null"}], "description": "Team that owns this social set. Null if the social set is owned by an individual user."}}, "required": ["id", "username", "name", "profile_image_url"], "title": "SocialSetListResponse", "type": "object"}, "Team": {"description": "Details of the team that owns this social set", "properties": {"id": {"description": "Unique identifier for the team", "examples": ["abc123def4567890"], "title": "Id", "type": "string"}, "name": {"description": "Name of the team", "examples": ["Marketing Team", "Engineering"], "title": "Name", "type": "string"}}, "required": ["id", "name"], "title": "Team", "type": "object"}, "BlueskyAccount": {"description": "Schema for Bluesky account data in API responses", "properties": {"platform": {"default": "bluesky", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"description": "Bluesky username/handle", "title": "Username", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Display name for the account. Null if not available.", "title": "Name"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"description": "URL to the public Bluesky profile", "title": "Profile Url", "type": "string"}}, "required": ["username", "profile_url"], "title": "BlueskyAccount", "type": "object"}, "LinkedInAccount": {"description": "Schema for LinkedIn account data in API responses", "properties": {"platform": {"default": "linkedin", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"description": "LinkedIn username/vanity name", "title": "Username", "type": "string"}, "name": {"description": "Display name for the account", "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"description": "URL to the public LinkedIn profile", "title": "Profile Url", "type": "string"}}, "required": ["username", "name", "profile_url"], "title": "LinkedInAccount", "type": "object"}, "MastodonAccount": {"description": "Schema for Mastodon account data in API responses", "properties": {"platform": {"default": "mastodon", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"description": "Mastodon username/handle (without the server suffix)", "title": "Username", "type": "string"}, "name": {"description": "Display name for the account", "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"description": "URL to the public Mastodon profile", "title": "Profile Url", "type": "string"}, "followers_count": {"description": "Number of followers on Mastodon", "title": "Followers Count", "type": "integer"}, "server": {"description": "Server/instance for the Mastodon account", "title": "Server", "type": "string"}}, "required": ["username", "name", "profile_url", "followers_count", "server"], "title": "MastodonAccount", "type": "object"}, "PlatformsDict": {"description": "Strictly typed dictionary for platform accounts", "properties": {"x": {"anyOf": [{"$ref": "#/components/schemas/XAccount"}, {"type": "null"}]}, "linkedin": {"anyOf": [{"$ref": "#/components/schemas/LinkedInAccount"}, {"type": "null"}]}, "mastodon": {"anyOf": [{"$ref": "#/components/schemas/MastodonAccount"}, {"type": "null"}]}, "threads": {"anyOf": [{"$ref": "#/components/schemas/ThreadsAccount"}, {"type": "null"}]}, "bluesky": {"anyOf": [{"$ref": "#/components/schemas/BlueskyAccount"}, {"type": "null"}]}, "substack": {"anyOf": [{"$ref": "#/components/schemas/SubstackAccount"}, {"type": "null"}]}}, "required": ["x", "linkedin", "mastodon", "threads", "bluesky", "substack"], "title": "PlatformsDict", "type": "object"}, "PublishingQuotaResponse": {"additionalProperties": false, "properties": {"used": {"description": "Number of published drafts already counted in the active window.", "title": "Used", "type": "integer"}, "remaining": {"anyOf": [{"type": "integer"}, {"const": "unlimited", "type": "string"}], "description": "Remaining publish slots in the active window, or \"unlimited\".", "title": "Remaining"}, "resets_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the current publishing quota window resets.", "title": "Resets At"}}, "required": ["used", "remaining"], "title": "PublishingQuotaResponse", "type": "object"}, "SocialSetDetailResponse": {"description": "Detailed social set response including all platform accounts", "properties": {"id": {"description": "Unique identifier for the social set", "examples": [12345, 67890], "title": "Id", "type": "integer"}, "username": {"description": "Username/handle for the social media account", "examples": ["elonmusk", "typefully"], "title": "Username", "type": "string"}, "name": {"description": "Display name of the social media account", "examples": ["Elon Musk", "Typefully"], "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image of the social media account. Null if not available.", "examples": ["https://typefully-user-avatars.s3.amazonaws.com/_generic/account/537/twitter.jpeg"], "title": "Profile Image Url"}, "team": {"anyOf": [{"$ref": "#/components/schemas/Team"}, {"type": "null"}], "description": "Team that owns this social set. Null if the social set is owned by an individual user."}, "platforms": {"$ref": "#/components/schemas/PlatformsDict", "description": "All platform accounts configured in this social set (X, LinkedIn, Mastodon, Threads, Bluesky)"}, "publishing_quota": {"anyOf": [{"$ref": "#/components/schemas/PublishingQuotaResponse"}, {"type": "null"}], "description": "Shared publishing quota snapshot for this social set."}}, "required": ["id", "username", "name", "profile_image_url", "platforms"], "title": "SocialSetDetailResponse", "type": "object"}, "SubstackAccount": {"description": "Schema for Substack account data in API responses", "properties": {"platform": {"default": "substack", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Substack handle. Null if not available.", "title": "Username"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Display name for the account. Null if not available.", "title": "Name"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the public Substack profile. Null if not available.", "title": "Profile Url"}}, "title": "SubstackAccount", "type": "object"}, "ThreadsAccount": {"description": "Schema for Threads account data in API responses", "properties": {"platform": {"default": "threads", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"description": "Threads username/handle", "title": "Username", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Display name for the account. Null if not available.", "title": "Name"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"description": "URL to the public Threads profile", "title": "Profile Url", "type": "string"}}, "required": ["username", "profile_url"], "title": "ThreadsAccount", "type": "object"}, "XAccount": {"description": "Schema for X/Twitter account data in API responses", "properties": {"platform": {"default": "x", "description": "Platform identifier", "title": "Platform", "type": "string"}, "username": {"description": "X/Twitter username/handle", "title": "Username", "type": "string"}, "name": {"description": "Display name for the account", "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the profile image. Null if not available.", "title": "Profile Image Url"}, "profile_url": {"description": "URL to the public X/Twitter profile", "title": "Profile Url", "type": "string"}}, "required": ["username", "name", "profile_url"], "title": "XAccount", "type": "object"}, "DraftOrderBy": {"description": "Allowed order_by fields for draft listing - prevents SQL injection", "enum": ["created_at", "-created_at", "updated_at", "-updated_at", "scheduled_date", "-scheduled_date", "published_at", "-published_at"], "title": "DraftOrderBy", "type": "string"}, "DraftListResponse": {"additionalProperties": false, "description": "Schema for draft/thread data in API responses with standardized timestamp field names", "properties": {"id": {"description": "Unique identifier for the draft", "examples": [12345], "title": "Id", "type": "integer"}, "preview": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Preview of the draft content (may be null for empty drafts).", "examples": ["Hello world", null], "title": "Preview"}, "scheduled_date": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Scheduled or planned datetime in UTC (ISO 8601); for planned drafts the date is inert. Null if the draft has no date.", "examples": ["2025-01-20T14:00:00Z", null], "title": "Scheduled Date"}, "draft_title": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Internal title for the draft. Null if not set.", "examples": ["My launch thread", null], "title": "Draft Title"}, "mastodon_post_enabled": {"description": "Whether posting to Mastodon is enabled for this draft", "examples": [true, false], "title": "Mastodon Post Enabled", "type": "boolean"}, "social_set_id": {"description": "ID of the social set (account) this draft belongs to", "examples": [67890], "title": "Social Set Id", "type": "integer"}, "share_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Public share URL for the draft. Null if the draft is not publicly shared.", "examples": ["https://typefully.com/share/abc123"], "title": "Share Url"}, "private_url": {"description": "Private URL for accessing the draft in Typefully. Can be shared with team members without requiring public sharing.", "examples": ["https://typefully.com/?d=12345&a=67890"], "title": "Private Url", "type": "string"}, "status": {"description": "Current status of the draft. 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed. Note: this reflects the stored draft lifecycle and does not flip to a 'publishing' value while an immediate publish is in flight - fetch the draft detail endpoint and read its `publish_state` to track that.", "title": "Status", "type": "string"}, "tags": {"description": "List of tag slugs (not names) associated with this draft. Use the /tags endpoint to get available tags with their slugs.", "examples": [["marketing", "product"], ["newsletter"], []], "items": {"type": "string"}, "title": "Tags", "type": "array"}, "created_at": {"description": "Timestamp when the draft was created (ISO 8601 format in UTC)", "examples": ["2025-01-15T10:30:00Z"], "format": "date-time", "title": "Created At", "type": "string"}, "updated_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the draft was last edited (ISO 8601 format in UTC). Null if never edited.", "examples": ["2025-01-16T09:15:00Z"], "title": "Updated At"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the draft was published on any enabled platform (ISO 8601 format in UTC). Null if not yet published anywhere.", "examples": ["2025-01-20T14:00:05Z"], "title": "Published At"}, "mastodon_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Mastodon (ISO 8601 format in UTC). Null if not published to Mastodon.", "examples": ["2025-01-20T14:00:05Z"], "title": "Mastodon Post Published At"}, "linkedin_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to LinkedIn (ISO 8601 format in UTC). Null if not published to LinkedIn.", "examples": ["2025-01-20T14:00:05Z"], "title": "Linkedin Post Published At"}, "threads_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Threads (ISO 8601 format in UTC). Null if not published to Threads.", "examples": ["2025-01-20T14:00:05Z"], "title": "Threads Post Published At"}, "bluesky_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Bluesky (ISO 8601 format in UTC). Null if not published to Bluesky.", "examples": ["2025-01-20T14:00:05Z"], "title": "Bluesky Post Published At"}, "substack_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the note was published to Substack (ISO 8601 format in UTC). Null if not published to Substack.", "examples": ["2025-01-20T14:00:05Z"], "title": "Substack Post Published At"}, "x_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to X (ISO 8601 format in UTC). Null if not published to X.", "examples": ["2025-01-20T14:00:05Z"], "title": "X Post Published At"}, "x_post_enabled": {"description": "Whether posting to X is enabled", "title": "X Post Enabled", "type": "boolean"}, "linkedin_post_enabled": {"description": "Whether posting to LinkedIn is enabled", "title": "Linkedin Post Enabled", "type": "boolean"}, "threads_post_enabled": {"description": "Whether posting to Threads is enabled", "title": "Threads Post Enabled", "type": "boolean"}, "bluesky_post_enabled": {"description": "Whether posting to Bluesky is enabled", "title": "Bluesky Post Enabled", "type": "boolean"}, "substack_post_enabled": {"description": "Whether posting to Substack is enabled", "title": "Substack Post Enabled", "type": "boolean"}, "x_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on X (Twitter). Null if not published to X or URL not available.", "examples": ["https://x.com/username/status/1234567890"], "title": "X Published Url"}, "linkedin_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on LinkedIn. Null if not published to LinkedIn or URL not available.", "examples": ["https://www.linkedin.com/feed/update/urn:li:share:1234567890"], "title": "Linkedin Published Url"}, "mastodon_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Mastodon. Null if not published to Mastodon or URL not available.", "examples": ["https://mastodon.social/@username/1234567890"], "title": "Mastodon Published Url"}, "threads_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Threads. Null if not published to Threads or URL not available.", "examples": ["https://www.threads.net/@username/post/ABC123"], "title": "Threads Published Url"}, "bluesky_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Bluesky. Null if not published to Bluesky or URL not available.", "examples": ["https://bsky.app/profile/username.bsky.social/post/abc123"], "title": "Bluesky Published Url"}, "substack_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published note on Substack. Null if not published to Substack or URL not available.", "examples": ["https://substack.com/@username/note/c-123456789"], "title": "Substack Published Url"}}, "required": ["id", "mastodon_post_enabled", "social_set_id", "private_url", "status", "created_at", "updated_at", "published_at", "mastodon_post_published_at", "linkedin_post_published_at", "threads_post_published_at", "bluesky_post_published_at", "substack_post_published_at", "x_post_enabled", "linkedin_post_enabled", "threads_post_enabled", "bluesky_post_enabled", "substack_post_enabled"], "title": "DraftListResponse", "type": "object"}, "PagedDraftListResponse": {"properties": {"results": {"items": {"$ref": "#/components/schemas/DraftListResponse"}, "title": "Results", "type": "array"}, "count": {"title": "Count", "type": "integer"}, "limit": {"title": "Limit", "type": "integer"}, "offset": {"title": "Offset", "type": "integer"}, "next": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next"}, "previous": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Previous"}}, "required": ["results", "count", "limit", "offset", "next", "previous"], "title": "PagedDraftListResponse", "type": "object"}, "BlueskyPostResponse": {"additionalProperties": false, "description": "Schema for Bluesky post content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}, "quote_post_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Bluesky-only: URL of the Bluesky post quoted by this post. Null when no quoted post is configured.", "title": "Quote Post Url"}}, "required": ["text"], "title": "BlueskyPostResponse", "type": "object"}, "BlueskySettings": {"additionalProperties": false, "description": "Settings specific to Bluesky", "properties": {}, "title": "BlueskySettings", "type": "object"}, "DisabledPlatform": {"additionalProperties": false, "description": "Shared schema for all disabled platforms", "properties": {"enabled": {"const": false, "title": "Enabled", "type": "boolean"}}, "required": ["enabled"], "title": "DisabledPlatform", "type": "object"}, "DraftDetailResponse": {"description": "Response schema for draft creation and retrieval", "properties": {"id": {"description": "Unique identifier for the draft", "examples": [12345], "title": "Id", "type": "integer"}, "social_set_id": {"description": "ID of the social set (account) this draft belongs to", "examples": [67890], "title": "Social Set Id", "type": "integer"}, "draft_id": {"deprecated": true, "description": "Deprecated: Use 'id' instead. Unique identifier for the draft.", "examples": [12345], "title": "Draft Id", "type": "integer"}, "status": {"description": "Current status of the draft. 'draft' = saved but not scheduled. 'scheduled' = queued to auto-publish at its scheduled_date. 'planned' = dated but inert: it has a scheduled_date but will NOT auto-publish until confirmed (by setting publish_at). A planned draft whose scheduled_date has passed is NOT overdue and NOT a failure - it simply hasn't been confirmed; replan it or confirm it. 'publishing' = a publish is in flight (transient). 'published' = successfully posted. 'error' = publishing failed. This reflects the stored draft lifecycle; it does not flip to 'publishing' while an immediate publish is in flight - use `publish_state` to track that.", "enum": ["draft", "scheduled", "published", "publishing", "error", "planned"], "examples": ["draft", "scheduled", "published"], "title": "Status", "type": "string"}, "publish_state": {"anyOf": [{"enum": ["in_progress", "finished"], "type": "string"}, {"type": "null"}], "description": "Async publish-progress signal, separate from `status`. null = no publish initiated; 'in_progress' = at least one platform is currently being posted; 'finished' = publishing has completed for all platforms. 'finished' means the job is done, not that it succeeded - read `status` and the per-platform published URLs (x_published_url, etc.) for the outcome. After publish_at=\"now\", poll GET /drafts/{id} until publish_state is 'finished'.", "examples": [null, "in_progress", "finished"], "title": "Publish State"}, "created_at": {"description": "Timestamp when the draft was created (ISO 8601 format in UTC)", "examples": ["2025-01-15T10:30:00Z"], "format": "date-time", "title": "Created At", "type": "string"}, "updated_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the draft was last edited (ISO 8601 format in UTC). Null if never edited.", "examples": ["2025-01-16T09:15:00Z"], "title": "Updated At"}, "scheduled_date": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the draft is scheduled to publish - or, for planned drafts, the inert planned date (ISO 8601 in UTC). Null if the draft has no date.", "examples": ["2025-01-20T14:00:00Z"], "title": "Scheduled Date"}, "published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the draft was published on any enabled platform (ISO 8601 format in UTC). Null if not yet published.", "examples": ["2025-01-20T14:00:05Z"], "title": "Published At"}, "draft_title": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Human-readable title for the draft. This is for internal organization only and is not posted to social media.", "examples": ["Weekly Newsletter", "Product Launch Announcement"], "title": "Draft Title"}, "tags": {"description": "List of tag slugs (not names) associated with this draft. Use the /tags endpoint to get available tags with their slugs.", "examples": [["marketing", "product"], ["newsletter"], []], "items": {"type": "string"}, "title": "Tags", "type": "array"}, "preview": {"description": "Text preview of the draft, smart-trimmed with a 100-character limit", "examples": ["Excited to announce our new feature! \ud83d\ude80"], "title": "Preview", "type": "string"}, "share_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Public share URL for the draft. Null if the draft is not publicly shared.", "examples": ["https://typefully.com/share/abc123"], "title": "Share Url"}, "private_url": {"description": "Private URL for accessing the draft in Typefully. Can be shared with team members without requiring public sharing.", "examples": ["https://typefully.com/?d=12345&a=67890"], "title": "Private Url", "type": "string"}, "platforms": {"$ref": "#/components/schemas/PlatformsResponse", "description": "Platform configurations showing which platforms are enabled and their content"}, "x_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on X (Twitter). Null if not published to X or URL not available.", "examples": ["https://x.com/username/status/1234567890"], "title": "X Published Url"}, "linkedin_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on LinkedIn. Null if not published to LinkedIn or URL not available.", "examples": ["https://www.linkedin.com/feed/update/urn:li:share:1234567890"], "title": "Linkedin Published Url"}, "mastodon_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Mastodon. Null if not published to Mastodon or URL not available.", "examples": ["https://mastodon.social/@username/1234567890"], "title": "Mastodon Published Url"}, "threads_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Threads. Null if not published to Threads or URL not available.", "examples": ["https://www.threads.net/@username/post/ABC123"], "title": "Threads Published Url"}, "bluesky_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published post on Bluesky. Null if not published to Bluesky or URL not available.", "examples": ["https://bsky.app/profile/username.bsky.social/post/abc123"], "title": "Bluesky Published Url"}, "substack_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published note on Substack. Null if not published to Substack or URL not available.", "examples": ["https://substack.com/@username/note/c-123456789"], "title": "Substack Published Url"}, "x_article_published_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL of the published X Article. Null if not published or URL not available.", "examples": ["https://x.com/i/article/1234567890"], "title": "X Article Published Url"}, "x_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to X  (ISO 8601 format in UTC). Null if not published to X.", "examples": ["2025-01-20T14:00:05Z"], "title": "X Post Published At"}, "linkedin_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to LinkedIn (ISO 8601 format in UTC). Null if not published to LinkedIn.", "examples": ["2025-01-20T14:00:08Z"], "title": "Linkedin Post Published At"}, "mastodon_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Mastodon (ISO 8601 format in UTC). Null if not published to Mastodon.", "examples": ["2025-01-20T14:00:10Z"], "title": "Mastodon Post Published At"}, "threads_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Threads (ISO 8601 format in UTC). Null if not published to Threads.", "examples": ["2025-01-20T14:00:12Z"], "title": "Threads Post Published At"}, "bluesky_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the post was published to Bluesky (ISO 8601 format in UTC). Null if not published to Bluesky.", "examples": ["2025-01-20T14:00:15Z"], "title": "Bluesky Post Published At"}, "substack_post_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the note was published to Substack (ISO 8601 format in UTC). Null if not published to Substack.", "examples": ["2025-01-20T14:00:15Z"], "title": "Substack Post Published At"}, "x_article_published_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "null"}], "description": "Timestamp when the X Article was published (ISO 8601 format in UTC). Null if not published.", "examples": ["2025-01-20T14:00:15Z"], "title": "X Article Published At"}, "scratchpad_text": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Plain text scratchpad notes for the draft. Formatting is stripped.", "examples": ["line 1\nline 2\n\nline 4"], "title": "Scratchpad Text"}}, "required": ["id", "social_set_id", "draft_id", "status", "created_at", "preview", "private_url", "platforms"], "title": "DraftDetailResponse", "type": "object"}, "EnabledBlueskyPlatformResponse": {"additionalProperties": false, "description": "Enabled Bluesky platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/BlueskyPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/BlueskySettings"}, {"type": "null"}], "description": "Bluesky-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledBlueskyPlatformResponse", "type": "object"}, "EnabledLinkedInPlatformResponse": {"additionalProperties": false, "description": "Enabled LinkedIn platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/LinkedInPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/LinkedInSettingsResponse"}, {"type": "null"}], "description": "LinkedIn-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledLinkedInPlatformResponse", "type": "object"}, "EnabledMastodonPlatformResponse": {"additionalProperties": false, "description": "Enabled Mastodon platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/MastodonPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/MastodonSettings"}, {"type": "null"}], "description": "Mastodon-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledMastodonPlatformResponse", "type": "object"}, "EnabledSubstackPlatformResponse": {"additionalProperties": false, "description": "Enabled Substack platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/SubstackPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/SubstackSettings"}, {"type": "null"}], "description": "Substack-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledSubstackPlatformResponse", "type": "object"}, "EnabledThreadsPlatformResponse": {"additionalProperties": false, "description": "Enabled Threads platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/ThreadsPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/ThreadsSettings"}, {"type": "null"}], "description": "Threads-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledThreadsPlatformResponse", "type": "object"}, "EnabledXPlatformResponse": {"additionalProperties": false, "description": "Enabled X platform for responses (no validation limits)", "properties": {"enabled": {"const": true, "title": "Enabled", "type": "boolean"}, "posts": {"description": "List of posts for this platform", "items": {"$ref": "#/components/schemas/XPostResponse"}, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/XSettings"}, {"type": "null"}], "description": "X-specific settings"}}, "required": ["enabled", "posts"], "title": "EnabledXPlatformResponse", "type": "object"}, "LinkedInPostResponse": {"additionalProperties": false, "description": "Schema for LinkedIn post content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "linkedin_reshare_urn": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "LinkedIn-only: canonical URN of the LinkedIn post reshared (repost) by this post. Null when no reshare is configured.", "title": "Linkedin Reshare Urn"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}}, "required": ["text"], "title": "LinkedInPostResponse", "type": "object"}, "LinkedInSettingsResponse": {"additionalProperties": false, "description": "LinkedIn settings in responses (no validation limits)", "properties": {"first_comment": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Comment posted from your LinkedIn account on the post right after it publishes. Null when not configured.", "title": "First Comment"}}, "title": "LinkedInSettingsResponse", "type": "object"}, "MastodonPostResponse": {"additionalProperties": false, "description": "Schema for Mastodon post content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}, "quote_post_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Mastodon-only: URL of the Mastodon status quoted by this post. Null when no quoted post is configured.", "title": "Quote Post Url"}}, "required": ["text"], "title": "MastodonPostResponse", "type": "object"}, "MastodonSettings": {"additionalProperties": false, "description": "Settings specific to Mastodon", "properties": {}, "title": "MastodonSettings", "type": "object"}, "PlatformsResponse": {"additionalProperties": false, "description": "Schema for platform configurations in responses (no validation limits)", "properties": {"x": {"anyOf": [{"$ref": "#/components/schemas/EnabledXPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "X (Twitter) configuration", "title": "X"}, "linkedin": {"anyOf": [{"$ref": "#/components/schemas/EnabledLinkedInPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "LinkedIn configuration", "title": "Linkedin"}, "mastodon": {"anyOf": [{"$ref": "#/components/schemas/EnabledMastodonPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "Mastodon configuration", "title": "Mastodon"}, "threads": {"anyOf": [{"$ref": "#/components/schemas/EnabledThreadsPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "Threads configuration", "title": "Threads"}, "bluesky": {"anyOf": [{"$ref": "#/components/schemas/EnabledBlueskyPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "Bluesky configuration", "title": "Bluesky"}, "substack": {"anyOf": [{"$ref": "#/components/schemas/EnabledSubstackPlatformResponse"}, {"$ref": "#/components/schemas/DisabledPlatform"}, {"type": "null"}], "description": "Substack Notes configuration", "title": "Substack"}, "x_article": {"anyOf": [{"$ref": "#/components/schemas/XArticlePlatformResponse"}, {"type": "null"}], "description": "X Article configuration"}}, "title": "PlatformsResponse", "type": "object"}, "SubstackPostResponse": {"additionalProperties": false, "description": "Schema for Substack note content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}, "quote_post_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Substack-only: URL of the Substack note or post restacked by this note. Null when no restack is configured.", "title": "Quote Post Url"}}, "required": ["text"], "title": "SubstackPostResponse", "type": "object"}, "SubstackSettings": {"additionalProperties": false, "description": "Settings specific to Substack", "properties": {}, "title": "SubstackSettings", "type": "object"}, "ThreadsPostResponse": {"additionalProperties": false, "description": "Schema for Threads post content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}, "quote_post_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Threads-only: URL of the Threads post quoted by this post. Null when no quoted post is configured.", "title": "Quote Post Url"}}, "required": ["text"], "title": "ThreadsPostResponse", "type": "object"}, "ThreadsSettings": {"additionalProperties": false, "description": "Settings specific to Threads", "properties": {}, "title": "ThreadsSettings", "type": "object"}, "XArticlePlatformResponse": {"additionalProperties": false, "description": "X Article platform data in responses.", "properties": {"content_markdown": {"description": "X Article Markdown. The first non-empty block must be `# Title`; remaining blocks are body. Supports paragraphs, blockquotes, lists, `#`/`##` headings, bold, italic, strikethrough, links, fenced code with an optional single-token language, and standalone `---`/`***` dividers (`GET` returns `---`). Code interiors stay literal. Escape delimiter-only prose such as `\\---`. Embeds must be alone on a line: `<typ:media media_id=\"...\" />`, `<typ:x-post url=\"https://x.com/user/status/123\" />`. Preserve comment-thread marker tags from `GET` when PATCHing. `GET` returns canonical Markdown, not submitted bytes.", "title": "Content Markdown", "type": "string"}, "cover_media_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Media ID for the X Article cover image, or null when no cover is configured.", "title": "Cover Media Id"}}, "required": ["content_markdown"], "title": "XArticlePlatformResponse", "type": "object"}, "XPostResponse": {"additionalProperties": false, "description": "Schema for X post content in API responses.", "properties": {"text": {"description": "The text content of the post", "title": "Text", "type": "string"}, "media_ids": {"description": "List of media IDs attached to the post.", "items": {"type": "string"}, "title": "Media Ids", "type": "array"}, "quote_post_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "X-only: URL of the X post quoted by this post. Null when no quoted post is configured.", "title": "Quote Post Url"}, "subscribers_only": {"default": false, "description": "X-only: Whether this individual post is visible only to paying Subscribers.", "examples": [true, false], "title": "Subscribers Only", "type": "boolean"}, "reply_settings": {"anyOf": [{"enum": ["following", "mentionedUsers", "subscribers", "verified"], "type": "string"}, {"type": "null"}], "description": "X-only: Who can reply to this post. Null means X's default (everyone can reply).", "examples": ["following", null], "title": "Reply Settings"}, "paid_partnership": {"default": false, "description": "X-only: Whether this post is labeled as a paid partnership.", "examples": [true, false], "title": "Paid Partnership", "type": "boolean"}, "made_with_ai": {"default": false, "description": "X-only: Whether this post is labeled as made with AI.", "examples": [true, false], "title": "Made With Ai", "type": "boolean"}, "hide_link_preview": {"default": false, "description": "Whether the link-preview card is suppressed for this post.", "title": "Hide Link Preview", "type": "boolean"}}, "required": ["text"], "title": "XPostResponse", "type": "object"}, "XSettings": {"additionalProperties": false, "description": "Settings specific to X (Twitter)", "properties": {"reply_to_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "URL of the X post to reply to. When provided, the first post in your thread will be posted as a reply.", "examples": ["https://x.com/therajatkapoor/status/1399394576951959554"], "title": "Reply To Url"}, "community_id": {"anyOf": [{"maxLength": 255, "type": "string"}, {"type": "null"}], "description": "ID of the X community to post to. Find the ID in the community URL (e.g., x.com/i/communities/1493446837214187523). You must have permission to post to the community, otherwise publishing will fail.", "title": "Community Id"}, "share_with_followers": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "When posting to a community, whether to also share the post to your timeline/followers. Defaults to true if not specified. Only has an effect when community_id is provided.", "title": "Share With Followers"}}, "title": "XSettings", "type": "object"}, "BlueskyPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for Bluesky", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Posts for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/BlueskyPost"}, "maxItems": 50, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/BlueskySettings"}, {"type": "null"}], "description": "Bluesky-specific settings"}}, "required": ["enabled"], "title": "BlueskyPlatform", "type": "object"}, "BlueskyPost": {"additionalProperties": false, "description": "Bluesky post.", "properties": {"text": {"description": "The text content of the post.", "examples": ["Hello world! This is my first post.", "Check out this amazing update! \ud83d\ude80"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "quote_post_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "bsky.app URL (/profile/<handle>/post/<rkey>) to quote, if its author allows quoting.", "title": "Quote Post Url"}}, "required": ["text"], "title": "BlueskyPost", "type": "object"}, "DraftCreateRequest": {"additionalProperties": false, "description": "Request schema for creating a draft", "properties": {"platforms": {"$ref": "#/components/schemas/Platforms", "description": "Platform configurations for each social media platform"}, "draft_title": {"anyOf": [{"maxLength": 512, "type": "string"}, {"type": "null"}], "description": "Draft title, for internal organization only; not posted to social media.", "examples": ["Weekly Newsletter", "Product Launch Announcement", "Monday Motivation Post"], "title": "Draft Title"}, "scratchpad_text": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Plain text scratchpad notes for the draft. Formatting is stripped.", "examples": ["line 1\nline 2\n\nline 4"], "title": "Scratchpad Text"}, "tags": {"description": "Tag slugs (not names). Tags must already exist in the social set - list them via /tags.", "examples": [["marketing", "product"], ["newsletter"], []], "items": {"type": "string"}, "maxItems": 10, "title": "Tags", "type": "array"}, "share": {"default": false, "description": "Whether to generate a public share URL for this draft. When true, anyone with the URL can view the draft content.", "title": "Share", "type": "boolean"}, "publish_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"enum": ["now", "next-free-slot"], "type": "string"}, {"type": "null"}], "description": "When to publish: \"now\" (immediate), \"next-free-slot\" (next available posting slot), or a future ISO 8601 datetime with timezone. Omit to save as a draft. Mutually exclusive with `plan_at`. \"now\" is asynchronous: the response returns `publish_state`=\"in_progress\" while `status` stays \"draft\" and published URLs are null - success, not failure; poll GET /drafts/{id} until `publish_state`=\"finished\", then read `status` and the published URLs.", "examples": ["2027-12-20T09:00:00-05:00", "now", "next-free-slot"], "title": "Publish At"}, "plan_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"const": "next-free-slot", "type": "string"}, {"type": "null"}], "description": "When to plan the draft. A planned draft is dated but inert: it shows on the queue and calendar at its date but never auto-publishes until confirmed by later setting `publish_at`. Accepts \"next-free-slot\" or a future ISO 8601 datetime with timezone (\"now\" is not valid). Mutually exclusive with `publish_at`. Omit to save as a plain draft.", "examples": ["2027-12-20T09:00:00-05:00", "next-free-slot"], "title": "Plan At"}}, "required": ["platforms"], "title": "DraftCreateRequest", "type": "object"}, "LinkedInPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for LinkedIn", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Posts for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/LinkedInPost"}, "maxItems": 50, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/LinkedInSettings"}, {"type": "null"}], "description": "LinkedIn-specific settings"}}, "required": ["enabled"], "title": "LinkedInPlatform", "type": "object"}, "LinkedInPost": {"additionalProperties": false, "description": "Schema for individual LinkedIn post content.", "properties": {"text": {"description": "The text content of the post. You can tag companies with mention syntax: @[Company Name](urn:li:organization:123456).", "examples": ["Thanks @[Typefully](urn:li:organization:86779668) for the support!"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "linkedin_reshare_target": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "Canonical URN or full LinkedIn post URL to reshare (repost). URLs are normalized to the canonical URN in storage and responses. Use values like urn:li:share:<id>, urn:li:ugcPost:<id>, or urn:li:groupPost:<id>.", "examples": ["urn:li:share:7437089188157554688"], "title": "Linkedin Reshare Target"}}, "required": ["text"], "title": "LinkedInPost", "type": "object"}, "LinkedInSettings": {"additionalProperties": false, "description": "Settings specific to LinkedIn", "properties": {"first_comment": {"anyOf": [{"maxLength": 1250, "type": "string"}, {"type": "null"}], "description": "Posted as a comment once the post publishes; null or empty removes it.", "title": "First Comment"}}, "title": "LinkedInSettings", "type": "object"}, "MastodonPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for Mastodon", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Posts for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/MastodonPost"}, "maxItems": 50, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/MastodonSettings"}, {"type": "null"}], "description": "Mastodon-specific settings"}}, "required": ["enabled"], "title": "MastodonPlatform", "type": "object"}, "MastodonPost": {"additionalProperties": false, "description": "Mastodon post.", "properties": {"text": {"description": "The text content of the post.", "examples": ["Hello world! This is my first post.", "Check out this amazing update! \ud83d\ude80"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "quote_post_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "Status URL on any instance to quote; your own server must support quotes and see it.", "title": "Quote Post Url"}}, "required": ["text"], "title": "MastodonPost", "type": "object"}, "Platforms": {"additionalProperties": false, "description": "Schema for all platform configurations", "properties": {"x": {"anyOf": [{"$ref": "#/components/schemas/XPlatform"}, {"type": "null"}], "description": "X (Twitter) configuration"}, "linkedin": {"anyOf": [{"$ref": "#/components/schemas/LinkedInPlatform"}, {"type": "null"}], "description": "LinkedIn configuration"}, "mastodon": {"anyOf": [{"$ref": "#/components/schemas/MastodonPlatform"}, {"type": "null"}], "description": "Mastodon configuration"}, "threads": {"anyOf": [{"$ref": "#/components/schemas/ThreadsPlatform"}, {"type": "null"}], "description": "Threads configuration"}, "bluesky": {"anyOf": [{"$ref": "#/components/schemas/BlueskyPlatform"}, {"type": "null"}], "description": "Bluesky configuration"}, "substack": {"anyOf": [{"$ref": "#/components/schemas/SubstackPlatform"}, {"type": "null"}], "description": "Substack Notes configuration"}, "x_article": {"anyOf": [{"$ref": "#/components/schemas/XArticlePlatform"}, {"type": "null"}], "description": "X Article configuration. This platform is standalone: do not combine it with other platforms."}}, "title": "Platforms", "type": "object"}, "SubstackPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for Substack Notes. Substack doesn't support threads, so `posts` must contain a single post.", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Single note for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/SubstackPost"}, "maxItems": 1, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/SubstackSettings"}, {"type": "null"}], "description": "Substack-specific settings"}}, "required": ["enabled"], "title": "SubstackPlatform", "type": "object"}, "SubstackPost": {"additionalProperties": false, "description": "Substack note.", "properties": {"text": {"description": "The text content of the post.", "examples": ["Hello world! This is my first post.", "Check out this amazing update! \ud83d\ude80"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "quote_post_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "Substack note (/note/c-<id>) or post (/p/<slug>) URL to restack.", "title": "Quote Post Url"}}, "required": ["text"], "title": "SubstackPost", "type": "object"}, "ThreadsPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for Threads", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Posts for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/ThreadsPost"}, "maxItems": 50, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/ThreadsSettings"}, {"type": "null"}], "description": "Threads-specific settings"}}, "required": ["enabled"], "title": "ThreadsPlatform", "type": "object"}, "ThreadsPost": {"additionalProperties": false, "description": "Threads post.", "properties": {"text": {"description": "The text content of the post.", "examples": ["Hello world! This is my first post.", "Check out this amazing update! \ud83d\ude80"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "quote_post_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "threads.net/threads.com post URL to quote: one of your own posts, or a recent post from a public profile once your Threads connection allows looking up public profiles.", "title": "Quote Post Url"}}, "required": ["text"], "title": "ThreadsPost", "type": "object"}, "XArticlePlatform": {"additionalProperties": false, "description": "Configuration for an X Article draft.", "properties": {"content_markdown": {"anyOf": [{"maxLength": 50000, "type": "string"}, {"type": "null"}], "description": "X Article Markdown. The first non-empty block must be `# Title`; remaining blocks are body. Supports paragraphs, blockquotes, lists, `#`/`##` headings, bold, italic, strikethrough, links, fenced code with an optional single-token language, and standalone `---`/`***` dividers (`GET` returns `---`). Code interiors stay literal. Escape delimiter-only prose such as `\\---`. Embeds must be alone on a line: `<typ:media media_id=\"...\" />`, `<typ:x-post url=\"https://x.com/user/status/123\" />`. Preserve comment-thread marker tags from `GET` when PATCHing. `GET` returns canonical Markdown, not submitted bytes. Required when creating an X Article draft; optional on PATCH when only changing `cover_media_id`.", "examples": ["# Think Different, Draft Different\n\nGreat drafts start when builders **question defaults**, *shape the rough edges*, and ~~wait for perfect certainty~~ publish what helps.\n\n> The best interface is the one readers forget they are using.\n\n# Working notes\n\n- Start with a sharp title\n- Use structure before decoration\n- Link only when [context helps](https://typefully.com)\n\n## Final pass\n\n1. Cut filler\n2. Keep the useful tension\n3. Ship the clearer version\n\n```python\nprint(\"Ship it\")\n```", "# My article title\n\nIntro paragraph.\n\n## Section\n\nBody text.\n\n<typ:media media_id=\"550e8400-e29b-41d4-a716-446655440000\" />\n\n<typ:x-post url=\"https://x.com/typefully/status/2025894220243063023\" />"], "title": "Content Markdown"}, "cover_media_id": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Optional media ID for the X Article cover image. Must reference an uploaded, ready, account-owned static image. Omit on PATCH to keep the current cover; send null to remove it.", "examples": ["550e8400-e29b-41d4-a716-446655440000"], "title": "Cover Media Id"}}, "title": "XArticlePlatform", "type": "object"}, "XPlatform": {"additionalProperties": false, "allOf": [{"if": {"properties": {"enabled": {"const": true}}, "required": ["enabled"]}, "then": {"required": ["posts"]}}], "description": "Configuration for X (Twitter)", "properties": {"enabled": {"description": "Required. Set true and include `posts` to publish to this platform; false (no `posts`) leaves it off.", "title": "Enabled", "type": "boolean"}, "posts": {"description": "Posts for this platform. Required when `enabled` is true; omit when false.", "items": {"$ref": "#/components/schemas/XPost"}, "maxItems": 50, "title": "Posts", "type": "array"}, "settings": {"anyOf": [{"$ref": "#/components/schemas/XSettings"}, {"type": "null"}], "description": "X-specific settings"}}, "required": ["enabled"], "title": "XPlatform", "type": "object"}, "XPost": {"additionalProperties": false, "description": "Schema for individual X post content.", "properties": {"text": {"description": "The text content of the post.", "examples": ["Hello world! This is my first post.", "Check out this amazing update! \ud83d\ude80"], "maxLength": 50000, "title": "Text", "type": "string"}, "media_ids": {"description": "Media IDs to attach to the post; obtain them via the media upload endpoint.", "examples": [["550e8400-e29b-41d4-a716-446655440000"]], "items": {"type": "string"}, "maxItems": 20, "title": "Media Ids", "type": "array"}, "hide_link_preview": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Hide the link-preview card (the URL stays as plain text); when false the last URL gets a preview. Omit or send null to keep the current value (false on create).", "title": "Hide Link Preview"}, "quote_post_url": {"anyOf": [{"maxLength": 2048, "type": "string"}, {"type": "null"}], "description": "URL of the X post to quote in this post (equivalent to Typefully's 'convert to quote' action). Accepts x.com and twitter.com URLs containing a /status/<id> segment.", "examples": ["https://x.com/typefully/status/2025894220243063023"], "title": "Quote Post Url"}, "subscribers_only": {"default": false, "description": "Visible only to paying Subscribers. Requires an X account approved for creator Subscriptions.", "title": "Subscribers Only", "type": "boolean"}, "reply_settings": {"anyOf": [{"enum": ["following", "mentionedUsers", "subscribers", "verified"], "type": "string"}, {"type": "null"}], "description": "Who can reply. Null (default) means everyone.", "title": "Reply Settings"}, "paid_partnership": {"default": false, "description": "Whether this post should be labeled as a paid partnership.", "title": "Paid Partnership", "type": "boolean"}, "made_with_ai": {"default": false, "description": "Whether this post should be labeled as made with AI.", "title": "Made With Ai", "type": "boolean"}}, "required": ["text"], "title": "XPost", "type": "object"}, "DraftUpdateRequest": {"additionalProperties": false, "description": "Request schema for updating a draft (partial updates supported)", "properties": {"platforms": {"anyOf": [{"$ref": "#/components/schemas/Platforms"}, {"type": "null"}], "description": "Platform configurations. Only provided platforms will be updated; omitted platforms remain unchanged."}, "draft_title": {"anyOf": [{"maxLength": 512, "type": "string"}, {"type": "null"}], "description": "Draft title, for internal organization only; not posted to social media. Omit to keep unchanged.", "examples": ["Weekly Newsletter", "Product Launch Announcement"], "title": "Draft Title"}, "scratchpad_text": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Plain text scratchpad notes for the draft. Formatting is stripped. Omit to keep unchanged.", "examples": ["line 1\nline 2\n\nline 4"], "title": "Scratchpad Text"}, "tags": {"anyOf": [{"items": {"type": "string"}, "maxItems": 10, "type": "array"}, {"type": "null"}], "description": "Tag slugs (not names). Tags must already exist in the social set - list them via /tags. Omit to keep unchanged.", "examples": [["marketing", "product"], ["newsletter"], []], "title": "Tags"}, "share": {"anyOf": [{"type": "boolean"}, {"type": "null"}], "description": "Whether to generate a public share URL. Omit to keep unchanged.", "title": "Share"}, "publish_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"enum": ["now", "next-free-slot"], "type": "string"}, {"type": "null"}], "description": "When to publish: \"now\" (immediate), \"next-free-slot\", or a future ISO 8601 datetime with timezone. On a planned draft, a datetime or \"next-free-slot\" confirms it into a real schedule (echo its `scheduled_date` to keep the date); \"now\" publishes it immediately. Explicit null returns it to plain draft status. Mutually exclusive with `plan_at`. Omit to keep unchanged. \"now\" is asynchronous: the response returns `publish_state`=\"in_progress\" (success, not failure); poll GET /drafts/{id} until \"finished\".", "examples": ["2027-12-20T09:00:00-05:00", "now", "next-free-slot"], "title": "Publish At"}, "plan_at": {"anyOf": [{"format": "date-time", "type": "string"}, {"const": "next-free-slot", "type": "string"}, {"type": "null"}], "description": "When to plan. A planned draft is dated but inert: on the queue and calendar but never auto-publishing until confirmed via `publish_at`. A future datetime with timezone or \"next-free-slot\" plans a plain draft, replans a planned one, or unschedules a scheduled one into a plan (requires publish access; \"now\" invalid). Explicit null returns a planned or scheduled draft to plain draft status. Mutually exclusive with `publish_at`. Omit to keep unchanged.", "examples": ["2027-12-20T09:00:00-05:00", "next-free-slot"], "title": "Plan At"}, "force_overwrite_comments": {"default": false, "description": "Comment-thread anchor preservation toggle. When false (the default), submitting `posts[*].text` or X Article `content_markdown` whose `<typ:comment-thread>` markers don't match the draft's stored comment threads is rejected with `409 COMMENTS_MARKER_MISMATCH`; re-include the missing markers and retry. When true, missing markers are accepted: their comment threads are resolved server-side and their anchors stripped; submitted markers still validate normally. Only JSON `true`/`false` (not `\"true\"` strings).", "title": "Force Overwrite Comments", "type": "boolean"}}, "title": "DraftUpdateRequest", "type": "object"}, "MediaUploadResponse": {"description": "Schema for media upload response", "properties": {"media_id": {"description": "Unique identifier for the uploaded media. Use this ID when attaching media to posts.", "examples": ["550e8400-e29b-41d4-a716-446655440000"], "format": "uuid", "title": "Media Id", "type": "string"}, "upload_url": {"description": "Presigned S3 URL for uploading the file. PUT the file content to this URL within 1 hour. ", "examples": ["https://s3.amazonaws.com/bucket/path/file?signature=..."], "title": "Upload Url", "type": "string"}}, "required": ["media_id", "upload_url"], "title": "MediaUploadResponse", "type": "object"}, "MediaUploadRequest": {"description": "Schema for media upload request", "properties": {"file_name": {"description": "Original filename with extension (e.g., 'image.jpg', 'video.mp4'). Used for MIME type detection and display. Allowed characters: letters, numbers, hyphens, underscores, periods, parentheses. Allowed extensions: .jpg, .jpeg, .png, .webp, .gif, .mp4, .mov, .pdf", "examples": ["profile-photo.jpg", "demo-video.mp4", "screenshot.png"], "maxLength": 255, "pattern": "^[a-zA-Z0-9_.()\\-]+\\.([jJ][pP][gG]|[jJ][pP][eE][gG]|[pP][nN][gG]|[wW][eE][bB][pP]|[gG][iI][fF]|[mM][pP]4|[mM][oO][vV]|[pP][dD][fF])$", "title": "File Name", "type": "string"}, "alt_text": {"anyOf": [{"maxLength": 1000, "type": "string"}, {"type": "null"}], "description": "Accessibility description for the media, used as alt text when publishing. Can also be set later with PATCH /media/{media_id}. Platform-specific character limits are validated when the media is attached to a draft.", "examples": ["A golden retriever catching a frisbee mid-air in a sunny park"], "title": "Alt Text"}}, "required": ["file_name"], "title": "MediaUploadRequest", "type": "object"}, "MediaStatusResponse": {"description": "Schema for media status response", "properties": {"media_id": {"description": "Unique identifier for the media file", "format": "uuid", "title": "Media Id", "type": "string"}, "file_name": {"description": "Original filename", "examples": ["photo.jpg"], "title": "File Name", "type": "string"}, "mime": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "MIME type of the media file", "examples": ["image/jpeg", "image/png", "video/mp4", "image/gif", "application/pdf"], "title": "Mime"}, "status": {"description": "Processing status: 'processing' = file is being processed, 'ready' = file is ready to use in posts, 'failed' = processing failed", "enum": ["processing", "ready", "failed"], "examples": ["ready", "processing", "failed"], "title": "Status", "type": "string"}, "error_reason": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Error message if status is 'failed'. Null otherwise.", "examples": ["Unsupported file format"], "title": "Error Reason"}, "alt_text": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Accessibility description for the media, used as alt text when publishing. Omitted when not set.", "examples": ["A golden retriever catching a frisbee mid-air in a sunny park"], "title": "Alt Text"}, "media_urls": {"anyOf": [{"additionalProperties": {"type": "string"}, "type": "object"}, {"type": "null"}], "description": "Dictionary of media URLs. For resizable images (PNG, JPG, WEBP): includes 'small' (600px), 'medium' (1200px), 'large' (3200px), and 'original'. For non-resizable media (GIF, video, PDF): includes only 'original'.", "examples": [{"large": "https://cdn.../resized/...", "medium": "https://cdn.../resized/...", "original": "https://cdn.../original/...", "small": "https://cdn.../resized/..."}], "title": "Media Urls"}}, "required": ["media_id", "file_name", "status"], "title": "MediaStatusResponse", "type": "object"}, "MediaUpdateRequest": {"description": "Schema for media update request", "properties": {"alt_text": {"anyOf": [{"maxLength": 1000, "type": "string"}, {"type": "null"}], "description": "Accessibility description for the media, used as alt text when publishing. Pass null to clear it. If omitted, the current value is kept.", "examples": ["A golden retriever catching a frisbee mid-air in a sunny park"], "title": "Alt Text"}}, "title": "MediaUpdateRequest", "type": "object"}, "PagedTagResponse": {"properties": {"results": {"items": {"$ref": "#/components/schemas/TagResponse"}, "title": "Results", "type": "array"}, "count": {"title": "Count", "type": "integer"}, "limit": {"title": "Limit", "type": "integer"}, "offset": {"title": "Offset", "type": "integer"}, "next": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Next"}, "previous": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Previous"}}, "required": ["results", "count", "limit", "offset", "next", "previous"], "title": "PagedTagResponse", "type": "object"}, "TagResponse": {"description": "Response schema for tag data", "properties": {"slug": {"description": "Auto-generated URL-safe identifier from the tag name", "examples": ["marketing", "product-launch", "weekly-updates"], "title": "Slug", "type": "string"}, "name": {"description": "Display name for the tag", "examples": ["Marketing", "Product Launch", "Weekly Updates"], "title": "Name", "type": "string"}, "created_at": {"description": "Timestamp when the tag was created (ISO 8601 format in UTC)", "examples": ["2025-01-15T10:30:00Z"], "format": "date-time", "title": "Created At", "type": "string"}}, "required": ["slug", "name", "created_at"], "title": "TagResponse", "type": "object"}, "TagCreateRequest": {"additionalProperties": false, "description": "Request schema for creating a tag", "examples": [{"name": "Marketing"}, {"name": "Product Launch"}, {"name": "Weekly Newsletter"}], "properties": {"name": {"description": "Display name for the tag. The slug will be auto-generated from this name.", "examples": ["Marketing", "Product Launch", "Weekly Updates"], "maxLength": 32, "minLength": 1, "title": "Name", "type": "string"}}, "required": ["name"], "title": "TagCreateRequest", "type": "object"}, "QueueScheduleResponse": {"additionalProperties": false, "description": "Response schema for queue schedule rules.", "properties": {"social_set_id": {"description": "ID of the social set (account) this schedule belongs to.", "examples": [123], "title": "Social Set Id", "type": "integer"}, "timezone": {"description": "Timezone name for this social set.", "examples": ["America/New_York"], "title": "Timezone", "type": "string"}, "rules": {"description": "Schedule rules in local time.", "items": {"$ref": "#/components/schemas/QueueScheduleRuleSchema"}, "title": "Rules", "type": "array"}}, "required": ["social_set_id", "timezone", "rules"], "title": "QueueScheduleResponse", "type": "object"}, "QueueScheduleRuleSchema": {"additionalProperties": false, "description": "Single schedule rule (local time + days of week).", "properties": {"h": {"description": "Hour in 24h clock (0-23)", "maximum": 23, "minimum": 0, "title": "H", "type": "integer"}, "m": {"description": "Minute (0-59)", "maximum": 59, "minimum": 0, "title": "M", "type": "integer"}, "days": {"description": "Days of week this rule applies to", "examples": [["mon", "tue", "wed", "thu", "fri"]], "items": {"enum": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "type": "string"}, "minItems": 1, "title": "Days", "type": "array"}}, "required": ["h", "m", "days"], "title": "QueueScheduleRuleSchema", "type": "object"}, "QueueScheduleUpdateRequest": {"additionalProperties": false, "description": "Request schema for fully replacing queue schedule rules.", "properties": {"rules": {"description": "New schedule rules (full replacement).", "items": {"$ref": "#/components/schemas/QueueScheduleRuleSchema"}, "title": "Rules", "type": "array"}}, "required": ["rules"], "title": "QueueScheduleUpdateRequest", "type": "object"}, "QueueDaySchema": {"additionalProperties": false, "description": "Queue day with its queue items.", "properties": {"date": {"description": "YYYY-MM-DD in the social set timezone", "title": "Date", "type": "string"}, "items": {"description": "Queue items for this day (in chronological order).", "items": {"$ref": "#/components/schemas/QueueItemSchema"}, "title": "Items", "type": "array"}}, "required": ["date", "items"], "title": "QueueDaySchema", "type": "object"}, "QueueItemSchema": {"additionalProperties": false, "description": "Queue items are time points in the queue.\n\n- `at` is an ISO8601 datetime in UTC.\n- `kind` differentiates schedule-generated queue slots from scheduled/planned drafts that do not occupy a slot.\n- `draft` is either null (free slot) or a DraftListResponse object.", "properties": {"at": {"description": "ISO8601 datetime in UTC.", "examples": ["2026-02-12T22:00:00Z"], "format": "date-time", "title": "At", "type": "string"}, "kind": {"description": "Queue item kind.\n\n- `queue_slot`: schedule-generated queue slot time (free slot if `draft` is null)\n- `custom_time`: scheduled or planned draft that does not count as a slot occupant (custom-time scheduling, or collision overflow)", "enum": ["queue_slot", "custom_time"], "examples": ["queue_slot"], "title": "Kind", "type": "string"}, "draft": {"anyOf": [{"$ref": "#/components/schemas/DraftListResponse"}, {"type": "null"}], "description": "Scheduled or planned draft occupying this time (check `status` to tell them apart). Null means the slot is free."}}, "required": ["at", "kind"], "title": "QueueItemSchema", "type": "object"}, "QueueResponse": {"additionalProperties": false, "description": "Response schema for the queue date-range view.", "properties": {"social_set_id": {"description": "ID of the social set (account) this queue belongs to.", "examples": [123], "title": "Social Set Id", "type": "integer"}, "start_date": {"description": "YYYY-MM-DD in the social set timezone", "examples": ["2026-02-01"], "title": "Start Date", "type": "string"}, "end_date": {"description": "YYYY-MM-DD in the social set timezone", "examples": ["2026-02-29"], "title": "End Date", "type": "string"}, "days": {"description": "Days in the requested date range (inclusive).", "items": {"$ref": "#/components/schemas/QueueDaySchema"}, "title": "Days", "type": "array"}}, "required": ["social_set_id", "start_date", "end_date", "days"], "title": "QueueResponse", "type": "object"}, "LinkedInOrganizationFromURLResponse": {"description": "Resolved LinkedIn organization metadata.", "properties": {"id": {"description": "LinkedIn organization ID", "examples": ["86779668"], "title": "Id", "type": "string"}, "urn": {"description": "LinkedIn organization URN, usable in mention syntax", "examples": ["urn:li:organization:86779668"], "title": "Urn", "type": "string"}, "mention_text": {"description": "Ready-to-use LinkedIn mention syntax to paste into post text.", "examples": ["@[Typefully](urn:li:organization:86779668)"], "title": "Mention Text", "type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Organization display name", "title": "Name"}, "vanity_name": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "LinkedIn vanity name", "title": "Vanity Name"}, "description": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Organization description", "title": "Description"}, "website": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Organization website", "title": "Website"}, "logo_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Organization logo URL", "title": "Logo Url"}, "url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Public LinkedIn company URL", "title": "Url"}}, "required": ["id", "urn", "mention_text"], "title": "LinkedInOrganizationFromURLResponse", "type": "object"}, "CommentResponse": {"additionalProperties": false, "description": "A single comment within a comment thread.", "properties": {"id": {"description": "Unique identifier for the comment.", "format": "uuid", "title": "Id", "type": "string"}, "text": {"description": "Plain-text comment body. Mentioning users is not supported.", "title": "Text", "type": "string"}, "created_at": {"description": "Timestamp when the comment was created (ISO 8601 in UTC).", "format": "date-time", "title": "Created At", "type": "string"}, "user": {"$ref": "#/components/schemas/CommentUserResponse", "description": "Author of the comment."}}, "required": ["id", "text", "created_at", "user"], "title": "CommentResponse", "type": "object"}, "CommentThreadListResponse": {"additionalProperties": false, "description": "Paginated list of comment threads for a draft.", "properties": {"results": {"description": "Comment threads in the current page.", "items": {"$ref": "#/components/schemas/CommentThreadResponse"}, "title": "Results", "type": "array"}, "count": {"description": "Total number of comment threads available.", "title": "Count", "type": "integer"}, "limit": {"description": "Items per page used for this request.", "title": "Limit", "type": "integer"}, "offset": {"description": "Current offset value.", "title": "Offset", "type": "integer"}, "next": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL for the next page (null on last page).", "title": "Next"}, "previous": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL for the previous page (null on first page).", "title": "Previous"}}, "required": ["results", "count", "limit", "offset"], "title": "CommentThreadListResponse", "type": "object"}, "CommentThreadResponse": {"additionalProperties": false, "description": "A comment thread (one root comment plus follow-up comments) on a draft.", "properties": {"id": {"description": "Unique identifier for the comment thread.", "format": "uuid", "title": "Id", "type": "string"}, "draft_id": {"description": "Identifier of the draft this comment thread belongs to.", "title": "Draft Id", "type": "integer"}, "platform": {"description": "Platform the comment thread is anchored on.", "enum": ["x", "linkedin", "mastodon", "threads", "bluesky", "substack", "x_article"], "title": "Platform", "type": "string"}, "status": {"description": "Resolution status of the comment thread.", "enum": ["unresolved", "resolved"], "title": "Status", "type": "string"}, "selected_text": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "The text the comment thread was originally anchored to. Captured when the thread was created and not re-derived afterwards, so it may not match the current post text if the post has been edited. Null for threads that were created against a whole post rather than a specific span.", "title": "Selected Text"}, "comments": {"description": "Comments in the thread, ordered by `created_at`.", "items": {"$ref": "#/components/schemas/CommentResponse"}, "title": "Comments", "type": "array"}}, "required": ["id", "draft_id", "platform", "status", "comments"], "title": "CommentThreadResponse", "type": "object"}, "CommentUserResponse": {"additionalProperties": false, "description": "Author shape attached to a comment.", "properties": {"id": {"description": "Unique identifier for the user.", "title": "Id", "type": "integer"}, "name": {"description": "Display name of the user.", "title": "Name", "type": "string"}, "profile_image_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "URL to the user's profile image. Null if not available.", "title": "Profile Image Url"}}, "required": ["id", "name"], "title": "CommentUserResponse", "type": "object"}, "CommentThreadCreateRequest": {"additionalProperties": false, "description": "Create a new comment thread anchored on a span of a draft post.", "properties": {"post_index": {"description": "Zero-based index of the target post within the platform's posts array.", "minimum": 0, "title": "Post Index", "type": "integer"}, "platform": {"anyOf": [{"enum": ["x", "linkedin", "mastodon", "threads", "bluesky", "substack"], "type": "string"}, {"type": "null"}], "description": "Required when the draft has multiple commentable platforms; otherwise resolves to the source platform.", "title": "Platform"}, "selected_text": {"description": "Exact substring (codepoint-equal) of the target platform's visible flat text the comment thread is anchored to. Copy verbatim from the GET response.\n\nLinkedIn mentions appear inside `posts[*].text` as `@[Name](urn:li:organization:ID)` or `@[Name](urn:li:person:ID)`. Mentions are indivisible \u2014 `selected_text` may either include the entire mention substring or stay outside it. A selection that starts or ends in the middle of a mention is rejected with `400 VALIDATION_ERROR`.", "maxLength": 50000, "minLength": 1, "title": "Selected Text", "type": "string"}, "occurrence": {"default": 0, "description": "Zero-based occurrence of `selected_text` within the post when the same substring appears multiple times.", "maximum": 10000, "minimum": 0, "title": "Occurrence", "type": "integer"}, "text": {"description": "Plain-text comment body. The server derives the stored rich_text from this.", "maxLength": 10000, "minLength": 1, "title": "Text", "type": "string"}}, "required": ["post_index", "selected_text", "text"], "title": "CommentThreadCreateRequest", "type": "object"}, "XArticleCommentThreadCreateRequest": {"additionalProperties": false, "description": "Create a new comment thread anchored on visible X Article text.", "properties": {"post_index": {"anyOf": [{"minimum": 0, "type": "integer"}, {"type": "null"}], "description": "Omit for X Article comments. If supplied with `platform: \"x_article\"`, it must be 0.", "title": "Post Index"}, "platform": {"const": "x_article", "description": "Use `x_article` to anchor the comment thread on the draft article.", "title": "Platform", "type": "string"}, "selected_text": {"description": "Exact substring (codepoint-equal) of the article's rendered visible text. Markdown syntax, media tags, and X post embed tags are not part of the match text.\n\nX Article text comments cannot overlap existing X Article text comments in this API version.", "maxLength": 50000, "minLength": 1, "title": "Selected Text", "type": "string"}, "occurrence": {"default": 0, "description": "Zero-based occurrence of `selected_text` within the article when the same substring appears multiple times.", "maximum": 10000, "minimum": 0, "title": "Occurrence", "type": "integer"}, "text": {"description": "Plain-text comment body. The server derives the stored rich_text from this.", "maxLength": 10000, "minLength": 1, "title": "Text", "type": "string"}}, "required": ["platform", "selected_text", "text"], "title": "XArticleCommentThreadCreateRequest", "type": "object"}, "CommentCreateRequest": {"additionalProperties": false, "description": "Add a comment to an existing comment thread.", "properties": {"text": {"description": "Plain-text comment body. The server derives the stored rich_text from this.", "maxLength": 10000, "minLength": 1, "title": "Text", "type": "string"}}, "required": ["text"], "title": "CommentCreateRequest", "type": "object"}, "CommentUpdateRequest": {"additionalProperties": false, "description": "Update the text body of a single comment within a comment thread.", "properties": {"text": {"description": "Plain-text comment body. The server derives the stored rich_text from this.", "maxLength": 10000, "minLength": 1, "title": "Text", "type": "string"}}, "required": ["text"], "title": "CommentUpdateRequest", "type": "object"}, "WebhookEventPayload": {"description": "Payload envelope sent to webhook endpoints.\n\nAll webhook events follow this structure, with the event type identifying\nthe specific event and data containing the full draft details.", "properties": {"event": {"description": "The event type that triggered this webhook", "enum": ["draft.created", "draft.scheduled", "draft.planned", "draft.published", "draft.status_changed", "draft.tags_changed", "draft.deleted"], "title": "Event", "type": "string"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event. See DraftDetailResponse schema for full structure."}}, "required": ["event", "data"], "title": "WebhookEventPayload", "type": "object"}}, "securitySchemes": {"PublicAPIAuthentication": {"type": "http", "scheme": "bearer"}}}, "servers": [{"url": "https://api.typefully.com", "description": "Production server"}], "tags": [{"name": "Users", "description": "Get details about the currently authenticated user."}, {"name": "Social Sets", "description": "Get details about social sets that you have access to."}, {"name": "Drafts", "description": "Get all your drafts and make changes to them.\n\n## Comment-thread markers in draft text\n\nIf a draft has comment threads, draft-detail `GET` and `PATCH` responses include Typefully comment-thread markers in `posts[*].text` and, for X Articles, in `platforms.x_article.content_markdown`. Treat these markers as structural anchor metadata, not user text. Preserve them exactly when editing so a `GET \u2192 modify \u2192 PATCH` round-trip keeps comment anchors attached.\n\nWhen you `PATCH /drafts/{id}`, the server compares submitted markers against the comment threads stored on the draft:\n\n- **Markers match** \u2192 request accepted; threads stay anchored to whatever text now sits inside the marker tags.\n- **Marker missing** (server has it, you didn't send it) \u2192 `409 COMMENTS_MARKER_MISMATCH` with a per-platform `missing[]` list. To deliberately drop the affected threads, retry with `force_overwrite_comments: true` \u2014 the threads are resolved server-side and their anchors stripped.\n- **Unknown marker id** (you sent an id not on this draft / platform / post) \u2192 `400 COMMENTS_MARKER_UNKNOWN_ID`. No force escape \u2014 fix the request.\n- **Malformed marker tag** \u2192 `400 COMMENTS_MARKER_MALFORMED` with `subcode` in details (`unbalanced`, `bad_uuid`, `bad_attr`, `bad_position`, `empty_span`, `mid_mention`, `depth_exceeded`, `count_exceeded`).\n\nConstraints on the marker grammar:\n\n- `id` must be a lowercase UUID. No other attributes are accepted.\n- Markers may be nested up to 16 levels deep, with at most 1000 markers per post.\n- Self-closing `<typ:comment-thread id=\"\u2026\"/>` must sit at paragraph start.\n- Span markers (`<typ:comment-thread id=\"\u2026\">\u2026</typ:comment-thread>`) must wrap at least one character.\n- Span markers may not start or end inside a LinkedIn mention (`@[Name](urn:li:organization:ID)` or `@[Name](urn:li:person:ID)`); mentions are atomic \u2014 the marker either contains the entire mention substring or stays outside it.\n\nRecommended edit flow:\n\n1. `GET /v2/social-sets/{social_set_id}/drafts/{draft_id}` without `exclude_comment_markers`.\n2. Modify draft text while preserving all comment-thread markers.\n3. `PATCH /v2/social-sets/{social_set_id}/drafts/{draft_id}` with `force_overwrite_comments: false` (the default).\n\n## Plain text for read-only flows\n\nFor display surfaces that don't round-trip \u2014 LLM context windows, CSV exports, dashboards \u2014 pass `?exclude_comment_markers=true` on `GET /drafts/{id}` or `PATCH /drafts/{id}`. The response renders draft text without marker tags. The flag is render-only and does NOT affect server-side validation on the request body. Content returned with this flag set **should not be PATCHed back** unless you intend to resolve or remove comment anchors."}, {"name": "Comments", "description": "List, create, edit, resolve, and delete comment threads on a draft \u2014 the same threads collaborators see in the webapp.\n\nA **comment thread** is the anchored discussion container. A **comment** is one message inside that thread. The canonical collection path is `/v2/social-sets/{social_set_id}/drafts/{draft_id}/comment-threads`.\n\n## Notable behaviors\n\n- **Listing.** `GET /comment-threads` defaults to `status=unresolved`, so resolved threads are omitted unless you request `status=resolved` or `status=all`.\n- **Anchoring.** A new thread anchors to a substring (`selected_text` + `occurrence`). Post-platform comments target a post in `posts[]`; X Article comments target commentable article text. The thread response carries the original `selected_text` snapshot \u2014 it does not change if the draft is edited later, and it may be `null` for older whole-post threads.\n- **X Article matching.** For `platform: \"x_article\"`, `selected_text` matches commentable article text in document order. Markdown syntax, media tags, X post embed tags, and fenced-code text are ignored. Code blocks do not support comment anchors; selecting code returns `400 VALIDATION_ERROR`. X Article text comments cannot overlap existing stored X Article anchors; overlapping selections return `400 VALIDATION_ERROR`.\n- **LinkedIn mentions** appear in `posts[*].text` as `@[Name](urn:li:organization:ID)` or `@[Name](urn:li:person:ID)`. A mention is indivisible, so `selected_text` must either contain the entire mention substring or stay outside it.\n- **Resolution is one-way.** There is no unresolve endpoint.\n- **Adding a comment to a resolved thread, or to a thread on an auto-synced platform, returns `409 CONFLICT`.**\n- **Comment threads cannot be created on auto-synced platforms** (autosync overwrites the post's content and would orphan the thread).\n- **Editing a comment is restricted to its author** \u2014 even users with WRITE access on the social set cannot edit someone else's comment.\n- **Root-comment delete cascades.** Deleting the *first (oldest)* comment in a thread deletes the entire thread. Use `DELETE /comment-threads/{id}` to delete a thread explicitly.\n- **Mentioning users in comment bodies via API v2 is not supported.** Editing a comment via API v2 clears any mentions the comment previously held.\n- **Hard limits.** 5,000 comment threads per draft and 500 comments per thread."}, {"name": "Queue", "description": "Inspect the queue (free slots + scheduled drafts) and manage the queue schedule for a social set.\n\n- Use `GET /v2/social-sets/{social_set_id}/queue` for date-range queue views.\n- Use `GET/PUT /v2/social-sets/{social_set_id}/queue/schedule` to read/replace slot rules."}, {"name": "Media", "description": "Allows you to upload media to use in your drafts. Supported formats: jpg, jpeg, png, webp, gif, mp4, mov, pdf.\n\nThe flow for uploading media is three-fold:\n1. Get an upload URL\n2. Upload the media to the URL\n3. Verify that the media has completed processing\n\nFor example scripts in Python and Node.js, see: https://gist.github.com/rajatkapoor/e5be4497506f58b3546a35925db6a3a8"}, {"name": "Tags", "description": "Get all your tags and or create new ones to easily organize your drafts."}, {"name": "Webhooks", "description": "Configure webhooks to receive real-time notifications when events occur in your account.\n\n## Webhook Events\n\nSubscribe to any combination of these events:\n\n| Event | Description |\n|-------|-------------|\n| `draft.created` | Triggered when a new draft is created |\n| `draft.planned` | Triggered when a draft is planned (dated but inert until confirmed) |\n| `draft.scheduled` | Triggered when a draft is scheduled for publishing |\n| `draft.published` | Triggered when a draft is successfully published |\n| `draft.status_changed` | Triggered on any status transition (includes planned/scheduled/published) |\n| `draft.tags_changed` | Triggered when draft tags are modified |\n| `draft.deleted` | Triggered when a draft is deleted |\n\n## Payload Structure\n\nAll webhooks send a JSON payload with two fields:\n- `event`: The event type (e.g., \"draft.created\")\n- `data`: The full draft details (see DraftDetailResponse schema)\n\n## HTTP Headers\n\nEach webhook request includes these headers:\n- `X-Typefully-Event`: The event type (e.g., \"draft.published\")\n- `X-Typefully-Timestamp`: Unix timestamp when the event was generated\n- `X-Typefully-Signature`: HMAC-SHA256 signature for verification (format: `sha256=<hex>`)\n\n## Signature Verification\n\nTo verify webhook authenticity, compute the expected signature and compare:\n\n```python\nimport hmac\nimport hashlib\n\ndef verify_signature(payload_body: bytes, timestamp: str, signature: str, webhook_secret: str) -> bool:\n    # Construct the signed payload\n    signature_payload = f\"{timestamp}.{payload_body.decode('utf-8')}\"\n\n    # Compute expected signature\n    expected = hmac.new(\n        webhook_secret.encode(),\n        signature_payload.encode(),\n        hashlib.sha256\n    ).hexdigest()\n\n    # Compare using constant-time comparison to prevent timing attacks\n    expected_sig = f\"sha256={expected}\"\n    return hmac.compare_digest(expected_sig, signature)\n```\n\n**Important notes:**\n- The JSON payload uses compact separators (no spaces after `:` or `,`) and sorted keys\n- Always use constant-time comparison to prevent timing attacks\n- Verify the timestamp is recent to prevent replay attacks\n\n## Retry Behavior\n\nFailed webhook deliveries are retried with exponential backoff up to 4 times (5 attempts total) over a 1 hour window.\n\nYour endpoint should return any 2xx status code to acknowledge receipt. Non-2xx responses trigger retries.\n\n## Auto-Disable\n\nWebhooks are automatically disabled after **100 consecutive failures**. You'll receive an email notification when this happens. Re-enable from your API settings after fixing the endpoint issue."}], "webhooks": {"draft.created": {"post": {"summary": "Draft Created", "description": "Triggered when a new draft is created in a social set you have access to.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.created", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.planned": {"post": {"summary": "Draft Planned", "description": "Triggered when a draft transitions into the planned status: dated on the queue and calendar but inert until confirmed into a real schedule.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.planned", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.scheduled": {"post": {"summary": "Draft Scheduled", "description": "Triggered when a draft is scheduled for publishing.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.scheduled", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.published": {"post": {"summary": "Draft Published", "description": "Triggered when a draft is successfully published to one or more platforms.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.published", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.status_changed": {"post": {"summary": "Draft Status Changed", "description": "Triggered on any status transition. This event fires alongside specific events like draft.planned, draft.scheduled, or draft.published.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.status_changed", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.tags_changed": {"post": {"summary": "Draft Tags Changed", "description": "Triggered when tags are added to or removed from a draft.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.tags_changed", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}, "draft.deleted": {"post": {"summary": "Draft Deleted", "description": "Triggered when a draft is deleted. The payload contains the draft data as it was before deletion.", "tags": ["Webhooks"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "description": "Payload envelope sent to webhook endpoints.", "required": ["event", "data"], "properties": {"event": {"type": "string", "const": "draft.deleted", "description": "The event type that triggered this webhook"}, "data": {"$ref": "#/components/schemas/DraftDetailResponse", "description": "The draft data at the time of the event."}}}}}}, "responses": {"200": {"description": "Return any 2xx status to acknowledge receipt"}}}}}}