{
  "openapi": "3.0.0",
  "info": {
    "title": "OmniSocials API",
    "version": "1.0.0",
    "description": "The OmniSocials REST API schedules, publishes and analyzes posts on 12 social platforms. Upload media, receive webhooks and answer your social inbox.\n\nIt covers 12 platforms and 13 channels, because LinkedIn has a personal profile and a company page: Instagram, Facebook, LinkedIn (profile and page), YouTube, TikTok, X, Pinterest, Threads, Bluesky, Mastodon, Google Business and Telegram.\n\n## Base URL\n\n```\nhttps://api.omnisocials.com/v1\n```\n\n## Authentication\n\nSend an API key as a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer omsk_live_your_api_key_here\n```\n\nCreate keys in the OmniSocials dashboard under **Settings > API**, and give each key only the scopes it needs. The [Authentication](https://docs.omnisocials.com/authentication) guide lists every scope.\n\n## Rate limits\n\nEach API key can make 100 requests per minute. Responses carry these headers:\n\n- `X-RateLimit-Limit`: requests allowed per minute\n- `X-RateLimit-Remaining`: requests left in the current window\n- `X-RateLimit-Reset`: Unix time, in seconds, when the window resets\n\nOver the limit, the API returns `429` with the code `rate_limit_exceeded` and a `Retry-After` header in seconds. See [Rate limits and errors](https://docs.omnisocials.com/rate-limits-and-errors)."
  },
  "servers": [
    {
      "url": "https://api.omnisocials.com/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Posts",
      "description": "Create, schedule, update, publish, and delete social media posts across every connected channel. This is the core of the OmniSocials API."
    },
    {
      "name": "Media",
      "description": "Upload and manage images, video, and PDFs (up to 100 MB per upload, 1 GB from a URL) for use in posts, reels, stories, and PDF carousels."
    },
    {
      "name": "Folders",
      "description": "Organize uploaded media into folders: list, create, and delete them to keep large asset libraries manageable."
    },
    {
      "name": "Hashtag Sets",
      "description": "Save reusable, named groups of hashtags per workspace and apply one to a new post in a single call, appended to captions or dropped into the auto first comment."
    },
    {
      "name": "Accounts",
      "description": "List and inspect the social accounts connected to a workspace across all 12 supported platforms."
    },
    {
      "name": "Analytics",
      "description": "Fetch post-level and account-level metrics (impressions, engagement, reach, and follower stats) in single or bulk requests."
    },
    {
      "name": "Approvals",
      "description": "Approval workflows (configured in the dashboard). List them here, route a new post through one with `approval_workflow_id` on POST /posts/create, and decide with POST /posts/{id}/approve or /reject."
    },
    {
      "name": "Webhooks",
      "description": "Manage webhook subscriptions for real-time events (post.scheduled, post.published, post.failed, post.approved, post.rejected). Deliveries are signed with HMAC-SHA256."
    },
    {
      "name": "Locations",
      "description": "Search and attach location tags (such as Instagram place tags) to posts."
    },
    {
      "name": "Audio",
      "description": "Search and attach Instagram audio tracks, including trending music, to reels."
    },
    {
      "name": "Pinterest",
      "description": "List and check the product Pins of the connected Pinterest account, to tag products on a Pin with `pinterest.product_tags`."
    },
    {
      "name": "Inbox",
      "description": "Read and reply to Instagram and Facebook DMs, comments, and mentions, LinkedIn company-page comments and mentions (LinkedIn has no API DMs), TikTok video comments (comments only; replies capped at 150 characters), YouTube video comments, X DMs for opted-in workspaces, and Threads replies and mentions. GET /inbox/next is a work queue: it serves the next item that needs an answer together with its conversation and the post it is on, and a reply sent with include_next chains straight to the following one. Comments on Facebook, Instagram, TikTok, YouTube and Threads can be hidden (POST /inbox/messages/{id}/hide); Facebook, Instagram and TikTok comments can be deleted (DELETE /inbox/messages/{id}). Replies sent from the native apps are mirrored into the inbox and count as answers. Reads require the inbox:read scope; replies, marking read, hiding and deleting require inbox:write. Replies are limited to existing conversations (no cold outreach) and to the platform messaging window. Each workspace can send up to 1,000 replies per rolling 24 hours; contact support@omnisocials.com for a higher limit."
    },
    {
      "name": "Automations",
      "description": "Comment-to-DM automations on the connected Instagram account: a keyword in a comment on a post (or in a DM, or in a story reply) gets a DM back, once per person, with an optional public reply under the comment. Create one for an OmniSocials post (`post_id`), for a post published outside OmniSocials (`instagram_media_id`), for the next post the workspace publishes, or for every post. Opt-in scopes `automations:read` and `automations:write`. A post can also carry its automation at create time (`comment_dm_automation` on POST /posts/create)."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key authentication. Get your key from Settings > API."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Error code for programmatic handling",
                "example": "not_found"
              },
              "message": {
                "type": "string",
                "description": "Human-readable error message",
                "example": "Post not found."
              },
              "details": {
                "type": "array",
                "description": "Optional structured details. Returned today by validation_error responses on POST /posts/create, POST /posts/create-and-publish, and POST /posts/{id}/publish when one or more media items violate a target platform's size/format limits. Each entry pinpoints a single platform/file combo so clients can surface inline errors.",
                "items": {
                  "type": "object",
                  "properties": {
                    "platform": {
                      "type": "string",
                      "description": "Channel that rejected the media (instagram, facebook, x, etc.)",
                      "example": "instagram"
                    },
                    "mediaUrl": {
                      "type": "string",
                      "description": "URL of the offending media item",
                      "example": "https://example.com/video.mov"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "media_too_large",
                        "media_format_unsupported",
                        "media_unreachable",
                        "media_type_unsupported"
                      ],
                      "description": "Machine-readable failure reason"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable explanation",
                      "example": "This video (450 MB) is too large for Instagram."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total number of items",
            "example": 42
          },
          "limit": {
            "type": "integer",
            "description": "Items per page",
            "example": 20
          },
          "offset": {
            "type": "integer",
            "description": "Number of items skipped",
            "example": 0
          },
          "has_more": {
            "type": "boolean",
            "description": "Whether there are more items",
            "example": true
          }
        }
      },
      "Post": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Post ID",
            "example": "1024"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "in_approval",
              "scheduled",
              "posting",
              "published",
              "failed",
              "warning",
              "rejected"
            ],
            "description": "`in_approval` = waiting for a reviewer in an approval workflow; PATCH /posts/{id} can still move its `schedule_at` without changing this status. `rejected` = an approver declined the post via POST /posts/{id}/reject; it will not be published. `posting` = a publish is in flight. `warning` = some platforms published and some failed (see `errors` for the failures, `published_urls` for the successes); retry the failures with POST /posts/{id}/retry. `failed` = every platform failed. NOTE: a failed post whose dashboard 'retry as new post' succeeded flips to `published` even though it has no `published_urls` of its own. Check `retries` for the post that actually carries the live URLs."
          },
          "type": {
            "type": "string",
            "enum": [
              "post",
              "story",
              "reel"
            ],
            "description": "Post type. 'reel' media must be a video (MP4/MOV): an image is rejected with 400 validation_error at schedule/publish; use 'post' to share an image. A 'story' carries 1 to 10 media items; every item is one slide and publishes as its own story, in order."
          },
          "story_slides": {
            "type": "object",
            "description": "Stories only, after publishing. Per-platform list of the published slides in publish order. `published_urls` keeps the first slide's URL per platform; this field has every slide.",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "index": {
                    "type": "integer",
                    "description": "Zero-based slide position.",
                    "example": 0
                  },
                  "native_post_id": {
                    "type": "string",
                    "description": "The slide's story id on the platform.",
                    "example": "17912345678901234"
                  },
                  "url": {
                    "type": "string",
                    "nullable": true,
                    "description": "Story URL (valid for 24 hours)."
                  },
                  "media_url": {
                    "type": "string",
                    "nullable": true
                  },
                  "media_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "video"
                    ]
                  },
                  "published_at": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-10-21T09:00:07.000Z"
                  }
                }
              }
            }
          },
          "content": {
            "type": "object",
            "description": "Post content per platform",
            "example": {
              "default": "Our autumn menu is here.",
              "instagram": "Our autumn menu is here. #autumnmenu"
            }
          },
          "accounts": {
            "type": "object",
            "description": "The channels the post targets, as a map of platform key to true or false. Every platform key is present; the channels the post publishes to are true. Requests send `accounts` as an array of account IDs instead (POST /posts/create, PATCH /posts/{id}).",
            "additionalProperties": {
              "type": "boolean"
            },
            "example": {
              "instagram": true,
              "linkedin_page": true,
              "facebook": false,
              "x": false
            }
          },
          "media": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-k3j9xq_0c9e6b1a-2f4d-4b8e-a7c3-9d1e5f6a2b70.jpg"
                  },
                  "id": {
                    "type": "string",
                    "example": "482913"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "alt": {
                    "type": "string",
                    "description": "Accessibility description (alt text) for this media item, when set.",
                    "example": "A bowl of pumpkin soup on a wooden table"
                  }
                }
              }
            },
            "description": "Per-platform media object. Keys: default, instagram, facebook, etc. Each value is an array of {url, id, channel, alt?}.",
            "example": {
              "default": [
                {
                  "url": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-k3j9xq_0c9e6b1a-2f4d-4b8e-a7c3-9d1e5f6a2b70.jpg",
                  "id": "482913",
                  "alt": "A bowl of pumpkin soup on a wooden table"
                }
              ]
            }
          },
          "location_id": {
            "type": "string",
            "description": "Instagram location tag (Facebook Place/Page ID), echoed back from the create/update request. Omitted when not set.",
            "example": "1234567890"
          },
          "collaborators": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "partnerbrand"
            },
            "description": "Instagram co-author usernames (the 'Collab' feature), echoed back from the create/update request. Omitted when none set."
          },
          "user_tags": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "username": {
                  "type": "string",
                  "example": "partnerbrand"
                },
                "x": {
                  "type": "number",
                  "example": 0.5
                },
                "y": {
                  "type": "number",
                  "example": 0.8
                },
                "image_index": {
                  "type": "integer",
                  "example": 0
                }
              }
            },
            "description": "Instagram photo user tags, echoed back from the create/update request. Omitted when none set."
          },
          "schedule_at": {
            "type": "string",
            "format": "date-time",
            "description": "Scheduled publish time (ISO 8601)",
            "example": "2026-10-21T09:00:00.000Z"
          },
          "app_url": {
            "type": "string",
            "format": "uri",
            "description": "Deep link to open this post inside the OmniSocials app, correct for the environment (staging vs production). Draft, scheduled, and failed posts open in the composer; published posts open in the details view. Hand this to the user so they can click straight through to review a draft you just created.",
            "example": "https://app.omnisocials.com/create-post/1024"
          },
          "published_urls": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            },
            "description": "Live URLs of the post on each platform after publishing. Keys are platform identifiers (`facebook`, `instagram`, `linkedin`, `linkedin_page`, `youtube`, `tiktok`, `pinterest`, `x`, `threads`, `bluesky`, `mastodon`, `google_business`). Only platforms that successfully published appear; failed platforms are omitted (see `errors`). Empty object `{}` while the post is still draft, scheduled, or processing.",
            "example": {
              "facebook": "https://www.facebook.com/123456789/posts/987654321",
              "instagram": "https://www.instagram.com/p/Cxyz123ABC/",
              "linkedin": "https://www.linkedin.com/feed/update/urn:li:share:7191234567890",
              "x": "https://x.com/yourhandle/status/1791234567890123456"
            }
          },
          "retry_of": {
            "type": "string",
            "example": "1023",
            "description": "Present when this post was created by the dashboard 'retry as new post' flow: the ID of the original failed post it retries. The live platform URLs are on THIS post's `published_urls`."
          },
          "retries": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "1031"
            },
            "description": "Present on a post that has been retried as a new post: the IDs of its retry posts (newest last). A `published` post with empty `published_urls` and `retries` set is a resolved failure: the actual publish lives on the retry post, so don't double-count it."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T14:32:10.000Z"
          },
          "approval_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "none",
              "pending",
              "approved",
              "rejected"
            ],
            "description": "Approval workflow status. `none` = no approval workflow attached. `pending` = waiting on an approver (post `status` is `in_approval`); act on it with POST /posts/{id}/approve or /reject. `approved` / `rejected` = the workflow finished. GET /posts/{id}/approval returns the review itself: steps, approvers, the rejection reason and the comments."
          },
          "errors": {
            "type": "object",
            "nullable": true,
            "additionalProperties": {
              "type": "string"
            },
            "description": "Per-platform user-friendly error messages, keyed by platform identifier (`facebook`, `instagram`, `linkedin`, `linkedin_page`, `youtube`, `tiktok`, `pinterest`, `x`, `threads`, `bluesky`, `mastodon`, `google_business`). Populated when `status` is `failed` or `warning`. Only platforms that failed appear; successful platforms are omitted (see `published_urls`). `null` while the post is still draft/scheduled/processing or every platform succeeded. A platform's entry is removed once a retry of it succeeds (via POST /posts/{id}/retry or the dashboard). Mirrors the per-platform error a user sees in the dashboard with a Retry button.",
            "example": {
              "facebook": "Already Posted: These photos were already posted.",
              "instagram": "Instagram couldn't accept one of your images. Check that every image is JPEG/PNG, at least 320×320, has an aspect ratio between 4:5 and 1.91:1, and isn't visually flagged by Instagram. Re-upload or replace the offending image and try again."
            }
          },
          "source": {
            "type": "string",
            "nullable": true,
            "example": "api",
            "description": "Origin of post creation: web, api, mcp, workflow, zapier, make, n8n, etc."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T14:35:42.000Z"
          },
          "x": {
            "type": "object",
            "nullable": true,
            "description": "X (Twitter) options on the post. Includes per-publish settings (reply_settings, made_with_ai, paid_partnership), the poll and the canonical thread shape. Present when the post has X-specific data; omitted otherwise.",
            "properties": {
              "reply_settings": {
                "type": "string",
                "enum": [
                  "",
                  "following",
                  "mentionedUsers"
                ]
              },
              "paid_partnership": {
                "type": "boolean",
                "example": false
              },
              "made_with_ai": {
                "type": "boolean",
                "example": false
              },
              "poll": {
                "$ref": "#/components/schemas/XPoll"
              },
              "thread_parts": {
                "type": "array",
                "description": "When the post is a chained X thread (2–25 parts), this returns the canonical tweet text in publish order. For single-tweet posts the array is omitted and the text lives in `content.x` (or `content.default`). Each part also includes any per-tweet `media_urls`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Stable client-side id for the part",
                      "example": "6f1d8e2a-4b7c-4d9e-8a3f-2c5b9e7d1a60"
                    },
                    "text": {
                      "type": "string",
                      "maxLength": 25000,
                      "description": "280 chars for standard accounts, 25,000 for X Premium/Premium+.",
                      "example": "1/3: our results are in"
                    },
                    "media_urls": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "bluesky": {
            "type": "object",
            "nullable": true,
            "description": "Bluesky options on the post. Present when the post is a Bluesky thread; omitted otherwise.",
            "properties": {
              "thread_parts": {
                "type": "array",
                "description": "When the post is a chained Bluesky thread (2–25 parts), this returns the canonical post text in publish order. For single posts the array is omitted and the text lives in `content.bluesky` (or `content.default`). Each part also includes any per-post `media_urls`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Stable client-side id for the part",
                      "example": "6f1d8e2a-4b7c-4d9e-8a3f-2c5b9e7d1a60"
                    },
                    "text": {
                      "type": "string",
                      "maxLength": 300,
                      "example": "1/3: our results are in"
                    },
                    "media_urls": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "mastodon": {
            "type": "object",
            "nullable": true,
            "description": "Mastodon options on the post. Present when the post is a Mastodon thread or carries a poll; omitted otherwise.",
            "properties": {
              "poll": {
                "$ref": "#/components/schemas/MastodonPoll"
              },
              "thread_parts": {
                "type": "array",
                "description": "When the post is a chained Mastodon thread (2–25 parts), this returns the canonical status text in publish order. For single posts the array is omitted and the text lives in `content.mastodon` (or `content.default`). Each part also includes any per-status `media_urls`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Stable client-side id for the part",
                      "example": "6f1d8e2a-4b7c-4d9e-8a3f-2c5b9e7d1a60"
                    },
                    "text": {
                      "type": "string",
                      "maxLength": 500,
                      "example": "1/3: our results are in"
                    },
                    "media_urls": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "threads": {
            "type": "object",
            "nullable": true,
            "description": "Threads options on the post. Present when the post is a Threads thread (multi-post chain), carries a location tag or a poll; omitted otherwise.",
            "properties": {
              "poll": {
                "$ref": "#/components/schemas/ThreadsPoll"
              },
              "location": {
                "type": "object",
                "nullable": true,
                "description": "Location tagged on the Threads post (sent as `location_id` to Threads at publish; on a multi-post thread it is tagged on the first post). Present only when set. `name`/`address`/`city`/`country` are whatever the caller stored alongside the id (the dashboard stores the search result; the API stores what you pass in `threads.location`).",
                "properties": {
                  "id": {
                    "type": "string",
                    "example": "17841400000000000"
                  },
                  "name": {
                    "type": "string",
                    "nullable": true,
                    "example": "Griffith Observatory"
                  },
                  "address": {
                    "type": "string",
                    "nullable": true,
                    "example": "2800 E Observatory Rd"
                  },
                  "city": {
                    "type": "string",
                    "nullable": true,
                    "example": "Los Angeles"
                  },
                  "country": {
                    "type": "string",
                    "nullable": true,
                    "example": "United States"
                  }
                }
              },
              "thread_parts": {
                "type": "array",
                "description": "When the post is a chained Threads thread (2 to 25 parts), this returns the canonical post text in publish order. For single posts the array is omitted and the text lives in `content.threads` (or `content.default`). Each part also includes any per-post `media_urls`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Stable client-side id for the part",
                      "example": "6f1d8e2a-4b7c-4d9e-8a3f-2c5b9e7d1a60"
                    },
                    "text": {
                      "type": "string",
                      "maxLength": 500,
                      "example": "1/3: our results are in"
                    },
                    "media_urls": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "telegram": {
            "type": "object",
            "nullable": true,
            "description": "Telegram options on the post, and after publishing the `result`. Omitted when none are set.",
            "properties": {
              "silent": {
                "type": "boolean",
                "example": false
              },
              "link_preview": {
                "type": "boolean",
                "example": true
              },
              "pin": {
                "type": "boolean",
                "example": true
              },
              "protect_content": {
                "type": "boolean",
                "example": false
              },
              "spoiler": {
                "type": "boolean",
                "example": false
              },
              "caption_above_media": {
                "type": "boolean",
                "example": false
              },
              "buttons": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TelegramButton"
                }
              },
              "poll": {
                "$ref": "#/components/schemas/TelegramPoll"
              },
              "result": {
                "type": "object",
                "description": "What was sent. Message ids are per chat.",
                "properties": {
                  "chat_id": {
                    "type": "string",
                    "example": "-1001234567890"
                  },
                  "message_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "42"
                    },
                    "description": "The message ids of the post (several for an album)."
                  },
                  "text_message_id": {
                    "type": "string",
                    "description": "The reply that carries text over 1,024 characters, when the post had media.",
                    "example": "43"
                  },
                  "pinned": {
                    "type": "boolean",
                    "example": true
                  },
                  "pin_error": {
                    "type": "string",
                    "enum": [
                      "no_right",
                      "failed"
                    ]
                  },
                  "link_public": {
                    "type": "string",
                    "nullable": true,
                    "description": "t.me/<username>/<id>, for public chats.",
                    "example": "https://t.me/brand/42"
                  },
                  "link_private": {
                    "type": "string",
                    "nullable": true,
                    "description": "t.me/c/<id>/<id>, which members can open and which survives a username change.",
                    "example": "https://t.me/c/1234567890/42"
                  },
                  "sent_at": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-10-21T09:00:07.000Z"
                  }
                }
              }
            }
          },
          "instagram": {
            "type": "object",
            "nullable": true,
            "description": "Instagram options on the post. Present when set; includes `first_comment` (the configured auto first comment) and, after publish, `first_comment_result` describing the outcome.",
            "properties": {
              "first_comment": {
                "type": "string",
                "description": "The configured first-comment text.",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              }
            }
          },
          "video_cover": {
            "allOf": [
              {
                "$ref": "#/components/schemas/VideoCover"
              }
            ],
            "nullable": true,
            "description": "The post's video cover exactly as stored; null when none was chosen."
          },
          "facebook": {
            "type": "object",
            "nullable": true,
            "description": "Facebook options on the post: `first_comment`, its post-publish `first_comment_result`, and the video thumbnail (`thumbnail_type` + `thumb_offset` / `cover_url`) when one is set.",
            "properties": {
              "first_comment": {
                "type": "string",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              }
            }
          },
          "linkedin": {
            "type": "object",
            "nullable": true,
            "description": "LinkedIn Profile options on the post: `first_comment`, its post-publish `first_comment_result`, `carousel_as_images` (present when the post opts out of the default PDF document carousel) and `document_source` (present as `slides` when the post opts out of sending the kept original PDF).",
            "properties": {
              "first_comment": {
                "type": "string",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              },
              "carousel_as_images": {
                "type": "boolean",
                "example": false
              },
              "document_source": {
                "type": "string",
                "enum": [
                  "slides"
                ]
              }
            }
          },
          "linkedin_page": {
            "type": "object",
            "nullable": true,
            "description": "LinkedIn Company Page options on the post: `first_comment`, its post-publish `first_comment_result`, `carousel_as_images` (present when the post opts out of the default PDF document carousel) and `document_source` (present as `slides` when the post opts out of sending the kept original PDF).",
            "properties": {
              "first_comment": {
                "type": "string",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              },
              "carousel_as_images": {
                "type": "boolean",
                "example": false
              },
              "document_source": {
                "type": "string",
                "enum": [
                  "slides"
                ]
              }
            }
          },
          "youtube": {
            "type": "object",
            "nullable": true,
            "description": "YouTube options on the post. After publish, `first_comment` and its `first_comment_result` are nested here alongside the Shorts metadata.",
            "properties": {
              "first_comment": {
                "type": "string",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              }
            }
          },
          "tiktok": {
            "type": "object",
            "nullable": true,
            "description": "TikTok options on the post. After publish, `first_comment` and its `first_comment_result` are nested here alongside the TikTok publish options. First comments need the workspace's TikTok comments authorization.",
            "properties": {
              "title": {
                "type": "string",
                "nullable": true,
                "description": "Photo Mode title (photo carousels only, max 90 characters). Absent when no title was set.",
                "example": "Autumn menu"
              },
              "first_comment": {
                "type": "string",
                "example": "Full menu at the link in our bio."
              },
              "first_comment_result": {
                "$ref": "#/components/schemas/FirstCommentResult"
              }
            }
          },
          "comment_dm_automation": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Automation"
              }
            ],
            "nullable": true,
            "description": "The comment-to-DM automation bound to this post, when one exists. Returned by GET /posts/{id} for a post that goes to Instagram, and by create and update when the request carried `comment_dm_automation`. Its `post` field is `null` here (the post is this one)."
          }
        }
      },
      "FirstCommentResult": {
        "type": "object",
        "description": "Outcome of an auto first-comment attempt, written after the main post publishes.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "posted",
              "failed",
              "skipped"
            ],
            "description": "`posted` = comment is live; `failed` = the main post published but the comment did not (see `error`; retry via POST /posts/retry-first-comment); `skipped` = the main post did not publish so the comment was never attempted."
          },
          "pending": {
            "type": "boolean",
            "nullable": true,
            "example": false,
            "description": "TikTok only. `true` while the comment waits for TikTok to return the final video id (status is `failed` with an explanatory error meanwhile); the comment is posted automatically once the id resolves, usually within minutes."
          },
          "comment_id": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "error": {
            "type": "string",
            "nullable": true
          },
          "posted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-10-21T09:00:07.000Z"
          }
        }
      },
      "VideoCoverEntry": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "frame",
              "custom"
            ],
            "description": "`frame` uses the video frame at `thumb_offset`; `custom` uploads the image at `cover_url`."
          },
          "thumb_offset": {
            "type": "integer",
            "minimum": 0,
            "description": "Milliseconds into the video (type `frame`).",
            "example": 3000
          },
          "cover_url": {
            "type": "string",
            "format": "uri",
            "description": "Public image URL, JPEG or PNG (type `custom`).",
            "example": "https://example.com/cover.jpg"
          }
        }
      },
      "VideoCover": {
        "allOf": [
          {
            "$ref": "#/components/schemas/VideoCoverEntry"
          },
          {
            "type": "object",
            "properties": {
              "overrides": {
                "type": "object",
                "description": "Per-platform overrides keyed by platform id (`instagram`, `facebook`, `linkedin`, `linkedin_page`, `tiktok`, `pinterest`, `youtube`). An override wins over the base cover for that platform.",
                "additionalProperties": {
                  "$ref": "#/components/schemas/VideoCoverEntry"
                }
              }
            }
          }
        ],
        "description": "Video thumbnail for a post whose media is one video. Applied on Instagram (feed videos and reels), Facebook (feed videos and reels), LinkedIn Profile and Page, TikTok, Pinterest and YouTube Shorts. On YouTube Shorts the cover shows only on channels where YouTube has enabled custom Shorts thumbnails (rolling out since July 2026, YouTube Partner Program channels first); on other channels YouTube stores it as the default thumbnail but shows a frame from the video on Shorts. TikTok only takes a frame, so a `custom` cover is skipped there (set a `tiktok` override with type `frame` to choose the frame). The per-platform reel fields (`instagram.thumb_offset` / `cover_url`, `tiktok.video_cover_timestamp_ms`, `pinterest.video_cover`) keep working and win over the base cover for their platform.",
        "example": {
          "type": "frame",
          "thumb_offset": 3000,
          "overrides": {
            "linkedin_page": {
              "type": "custom",
              "cover_url": "https://example.com/cover.jpg"
            }
          }
        }
      },
      "LinkedInPollFields": {
        "type": "object",
        "required": [
          "question",
          "options",
          "duration"
        ],
        "properties": {
          "question": {
            "type": "string",
            "maxLength": 140,
            "description": "The poll question.",
            "example": "Which feature should we build next?"
          },
          "options": {
            "type": "array",
            "minItems": 2,
            "maxItems": 4,
            "items": {
              "type": "string",
              "maxLength": 30,
              "example": "Bulk upload"
            },
            "description": "2-4 answer options."
          },
          "duration": {
            "type": "string",
            "enum": [
              "ONE_DAY",
              "THREE_DAYS",
              "SEVEN_DAYS",
              "FOURTEEN_DAYS"
            ],
            "description": "How long the poll stays open for votes."
          }
        }
      },
      "XPoll": {
        "type": "object",
        "description": "A poll on an X post. The tweet text is the question. Stored as `x.poll`.",
        "required": [
          "options"
        ],
        "properties": {
          "options": {
            "type": "array",
            "minItems": 2,
            "maxItems": 4,
            "items": {
              "type": "string",
              "maxLength": 25,
              "example": "Yes"
            },
            "description": "2 to 4 answer options of up to 25 characters, all different."
          },
          "duration_minutes": {
            "type": "integer",
            "minimum": 5,
            "maximum": 10080,
            "default": 1440,
            "description": "How long the poll stays open, in minutes (5 to 10080, 7 days)."
          }
        }
      },
      "ThreadsPoll": {
        "type": "object",
        "description": "A poll on a Threads post. The post text is the question; Threads closes the poll 24 hours after it is published. Stored as `threads.poll`.",
        "required": [
          "options"
        ],
        "properties": {
          "options": {
            "type": "array",
            "minItems": 2,
            "maxItems": 4,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 25,
              "example": "Yes"
            },
            "description": "2 to 4 answer options of 1 to 25 characters, all different."
          }
        }
      },
      "MastodonPoll": {
        "type": "object",
        "description": "A poll on a Mastodon status. The status text is the question. Limits come from the instance (Mastodon's defaults: 4 options of 50 characters, 5 minutes to 1 month). Stored as `mastodon.poll`.",
        "required": [
          "options"
        ],
        "properties": {
          "options": {
            "type": "array",
            "minItems": 2,
            "items": {
              "type": "string",
              "example": "Yes"
            },
            "description": "The answer options, all different; count and length within the instance's limits."
          },
          "expires_in": {
            "type": "integer",
            "default": 86400,
            "description": "How long the poll stays open, in seconds, within the instance's range (default one day)."
          },
          "multiple": {
            "type": "boolean",
            "default": false,
            "description": "Allow several answers per person."
          },
          "hide_totals": {
            "type": "boolean",
            "default": false,
            "description": "Hide the vote counts until the poll ends."
          }
        }
      },
      "TelegramPoll": {
        "type": "object",
        "description": "A poll in a Telegram channel or group. The post text is the question (up to 300 characters). Polls in a channel are anonymous. Stored as `telegram.poll`.",
        "required": [
          "options"
        ],
        "properties": {
          "options": {
            "type": "array",
            "minItems": 2,
            "maxItems": 12,
            "items": {
              "type": "string",
              "maxLength": 100,
              "example": "Yes"
            },
            "description": "2 to 12 answer options of up to 100 characters, all different."
          },
          "open_period": {
            "type": "integer",
            "minimum": 5,
            "maximum": 2628000,
            "description": "Seconds the poll stays open (5 seconds to about 30 days). Leave out to keep the poll open.",
            "example": 86400
          },
          "multiple": {
            "type": "boolean",
            "default": false,
            "description": "Allow several answers per person."
          }
        }
      },
      "TelegramButton": {
        "type": "object",
        "required": [
          "text",
          "url"
        ],
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 64,
            "description": "Button label.",
            "example": "See the menu"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Link the button opens: https://, http:// or tg://.",
            "example": "https://example.com/menu"
          }
        }
      },
      "CreatePostRequest": {
        "type": "object",
        "required": [
          "content",
          "accounts"
        ],
        "properties": {
          "content": {
            "type": "object",
            "description": "Post content. Use 'default' for all platforms, or platform-specific keys: instagram, facebook, linkedin, linkedin_page, youtube, tiktok, pinterest, x, threads, bluesky, mastodon, google_business, telegram. Coming soon: reddit, snapchat.",
            "example": {
              "default": "Hello world!",
              "instagram": "Hello from IG! #awesome"
            }
          },
          "accounts": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "instagram"
            },
            "description": "Social accounts to post to. Each entry is either:\n- A bare platform name (e.g. `\"youtube\"`, `\"linkedin_page\"`), or\n- The composite ID returned by `GET /v1/accounts` in the form `\"<workspace_id>_<platform>\"` (e.g. `\"844008_youtube\"`).\n\nThe composite form's workspace prefix must match the workspace your API key belongs to. Mismatched or unknown ids return 400 `validation_error` with an \"Unknown account/platform\" message. Always source ids from `GET /v1/accounts` for the API key in use.\n"
          },
          "type": {
            "type": "string",
            "enum": [
              "post",
              "story",
              "reel"
            ],
            "default": "post",
            "description": "Post type: 'post' (feed), 'story' (Instagram/Facebook), 'reel' (Instagram/Facebook/YouTube/TikTok/Telegram, where a reel is sent as a video message). A 'reel' must have video media (MP4/MOV); an image is rejected with 400 validation_error at schedule/publish. Use 'post' to share an image. A 'story' takes 1 to 10 media items: each item is one slide, published as its own story in the order given (Instagram and Facebook have no multi-slide story container). More than 10 returns 400 validation_error."
          },
          "media": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "string",
                      "example": "482913"
                    },
                    {
                      "type": "object",
                      "required": [
                        "id"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "482913"
                        },
                        "alt": {
                          "type": "string",
                          "maxLength": 1500,
                          "example": "A bowl of pumpkin soup on a wooden table",
                          "description": "Accessibility description (alt text) for this media item. Delivered to Mastodon (media description), Bluesky (embed alt), X (media metadata, photos/GIFs only), Pinterest (pin alt_text fallback), Instagram (images and carousel image slides, not Reels/Stories) and LinkedIn (images only, not video or documents)."
                        }
                      }
                    }
                  ]
                },
                "description": "Flat array of media IDs, the same for all platforms. Entries are bare IDs or { id, alt } objects. A media item of type `document` (a PDF kept as one item) expands into its page images, in order, so one id attaches the whole deck."
              },
              {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "type": "string",
                        "example": "482913"
                      },
                      {
                        "type": "object",
                        "required": [
                          "id"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "482913"
                          },
                          "alt": {
                            "type": "string",
                            "maxLength": 1500,
                            "example": "A bowl of pumpkin soup on a wooden table"
                          }
                        }
                      }
                    ]
                  }
                },
                "description": "Per-platform object with the keys 'default', 'instagram', 'pinterest', etc. Entries are bare IDs or { id, alt } objects."
              }
            ],
            "description": "Media IDs from POST /media/upload. Flat array or per-platform object. Any entry may be an { id, alt } object to carry a per-media accessibility description (alt text, max 1500 chars). For type 'story', each item is one slide (max 10, published in order)."
          },
          "media_urls": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "string",
                      "format": "uri",
                      "example": "https://example.com/photo.jpg"
                    },
                    {
                      "type": "object",
                      "required": [
                        "url"
                      ],
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "example": "https://example.com/photo.jpg"
                        },
                        "alt": {
                          "type": "string",
                          "maxLength": 1500,
                          "example": "A bowl of pumpkin soup on a wooden table",
                          "description": "Accessibility description (alt text) for this media item. Delivered to Mastodon (media description), Bluesky (embed alt), X (media metadata, photos/GIFs only), Pinterest (pin alt_text fallback), Instagram (images and carousel image slides, not Reels/Stories) and LinkedIn (images only, not video or documents)."
                        }
                      }
                    }
                  ]
                },
                "description": "Flat array, the same media for all platforms. Entries are URL strings or { url, alt } objects."
              },
              {
                "type": "object",
                "additionalProperties": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "type": "string",
                        "format": "uri",
                        "example": "https://example.com/photo.jpg"
                      },
                      {
                        "type": "object",
                        "required": [
                          "url"
                        ],
                        "properties": {
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "example": "https://example.com/photo.jpg"
                          },
                          "alt": {
                            "type": "string",
                            "maxLength": 1500,
                            "example": "A bowl of pumpkin soup on a wooden table"
                          }
                        }
                      }
                    ]
                  }
                },
                "description": "Per-platform object with the keys 'default' (fallback for all platforms), 'instagram', 'pinterest', etc. Selected platforms without an explicit key inherit from 'default'. Pass an empty array (e.g. 'facebook': []) to explicitly opt a platform out of media. Entries are URL strings or { url, alt } objects.",
                "example": {
                  "default": [
                    {
                      "url": "https://example.com/photo.jpg",
                      "alt": "A red bicycle leaning against a brick wall"
                    }
                  ],
                  "instagram": [
                    "https://example.com/square.jpg"
                  ]
                }
              }
            ],
            "description": "External image/video/PDF URLs to download and attach (max 10 total, each file ≤ 100 MB). Flat array or per-platform object. Any entry may be a { url, alt } object to carry a per-media accessibility description (alt text, max 1500 chars), delivered to Mastodon, Bluesky, X, Pinterest, Instagram (images) and LinkedIn (images). When using per-platform format, 'default' acts as fallback for selected platforms without their own key. A PDF URL is rasterized into one image slide per page (max 20, in order). On LinkedIn it publishes as a swipeable document, elsewhere as an image carousel; slides inherit the entry's alt. For files over 100 MB (up to 1 GB), upload first via POST /v1/media/upload-from-url and attach the returned media id via the `media` field; oversized media_urls fail with code `file_too_large`."
          },
          "schedule_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 datetime to schedule. Omit for a draft without a date. With `draft: true` the time is kept but the post stays a draft (a planned draft on the calendar, see `draft`).",
            "example": "2026-10-21T09:00:00Z"
          },
          "draft": {
            "type": "boolean",
            "description": "Planned draft. With `schedule_at`, the post is stored as `draft` and keeps that time: it shows on the dashboard calendar at that slot with a dashed outline, the scheduler never publishes it, and a person schedules it from the composer (or you do with `PATCH /posts/{id}` and `draft: false`). Use it when an agent proposes a slot that a human should confirm. Not allowed with `publish_now` or `approval_workflow_id` (400 `validation_error`). Without `schedule_at` it changes nothing: that post is a draft already.",
            "example": true
          },
          "media_fit": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "auto",
                "crop",
                "keep"
              ]
            },
            "description": "Image fit per channel, for feed images (type `post`) whose ratio is outside what a channel shows uncropped. `auto` (the default when omitted) takes the channel's rule: Instagram crops a tall or wide image to 4:5 / 1.91:1 around the subject (Instagram cuts it to that window anyway, and Meta cannot boost a feed post outside it); Pinterest keeps a Pin taller than 2:3 and only reports it. `crop` crops for that channel, `keep` sends the original. Every decision is reported in the response `warnings` with `code: \"image_fit\"`. The crop is a file of its own for that channel only; `default` and the other channels keep the original. On `PATCH`, `media_fit` alone (no media) re-decides the stored images: `keep` gives the original back, `crop` crops an image that was kept.",
            "example": {
              "instagram": "keep"
            }
          },
          "publish_now": {
            "type": "boolean",
            "description": "Set true to publish immediately",
            "example": false
          },
          "link_url": {
            "type": "string",
            "format": "uri",
            "description": "URL to share as a rich preview card on platforms that support link-share posts (currently LinkedIn and Facebook). When set on a text post, the URL renders as a preview tile with thumbnail / title / description instead of plain text. Ignored on platforms that don't support link shares (Instagram, TikTok, etc.) and on posts that already have media attached (media takes precedence).",
            "example": "https://goodlightai.com/blog/new-feature"
          },
          "link_title": {
            "type": "string",
            "description": "Optional title for the link-share preview. LinkedIn uses this directly when set. Facebook ignores this: it fetches OG metadata server-side. Omit to let LinkedIn auto-fetch the page title.",
            "example": "Our autumn menu"
          },
          "link_description": {
            "type": "string",
            "description": "Optional description for the link-share preview. LinkedIn uses this when set; Facebook auto-fetches the OG description. Omit to defer to OG metadata.",
            "example": "Six new dishes, available from today."
          },
          "link_thumbnail_url": {
            "type": "string",
            "format": "uri",
            "example": "https://example.com/preview.jpg",
            "description": "Optional thumbnail image URL for the preview card. LinkedIn uploads this image as the card thumbnail; without it, LinkedIn uses the page's Open Graph image, and when the upload fails the card has no image. Facebook ignores it and uses the page's Open Graph image."
          },
          "video_cover": {
            "$ref": "#/components/schemas/VideoCover"
          },
          "source": {
            "type": "string",
            "description": "Self-reported origin of the integration creating this post. Defaults to 'api' if omitted. Use 'zapier', 'make', 'n8n', etc. to identify your integration.",
            "example": "zapier"
          },
          "location_id": {
            "type": "string",
            "description": "Instagram only. Facebook Place/Page ID to tag the post's location with (e.g. a dealership or storefront). Applied to single-image and carousel feed posts via Instagram's content-publishing `location_id`. Find a Place's Page ID via Facebook's Pages Search API. Ignored by other platforms. Send an empty string on PATCH to clear it.",
            "example": "1234567890"
          },
          "collaborators": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "partnerbrand"
            },
            "maxItems": 3,
            "description": "Instagram only. Up to 3 public Instagram usernames to invite as co-authors (the 'Collab' feature). Applied to image, carousel, and reel posts, not Stories. Invited users get an invitation in the Instagram app; once accepted, the post also appears on their profile and feed. Usernames are case-insensitive and a leading '@' is stripped. Private accounts and non-existent usernames are rejected by Instagram at publish time. Ignored by other platforms. Send an empty array on PATCH to clear collaborators.",
            "example": [
              "therock",
              "kevinhart4real"
            ]
          },
          "user_tags": {
            "type": "array",
            "description": "Instagram only. Tag public Instagram accounts at specific positions on a photo. Photos only, not video, reels, or stories. For a single image omit `image_index` (or set 0); for a carousel, set `image_index` to the 0-based slide the tag belongs to (tags attach to that slide's image). Public accounts only; private/non-existent usernames are rejected by Instagram at publish time. Ignored by other platforms. Send an empty array on PATCH to clear tags.",
            "items": {
              "type": "object",
              "required": [
                "username",
                "x",
                "y"
              ],
              "properties": {
                "username": {
                  "type": "string",
                  "description": "Public Instagram username to tag. A leading '@' is stripped.",
                  "example": "partnerbrand"
                },
                "x": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Horizontal position of the tag, 0.0–1.0 from the photo's left edge.",
                  "example": 0.5
                },
                "y": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1,
                  "description": "Vertical position of the tag, 0.0–1.0 from the photo's top edge.",
                  "example": 0.8
                },
                "image_index": {
                  "type": "integer",
                  "minimum": 0,
                  "description": "0-based carousel slide the tag applies to. Defaults to 0 (the only image for a single-image post).",
                  "example": 0
                }
              }
            },
            "example": [
              {
                "username": "therock",
                "x": 0.5,
                "y": 0.8
              }
            ]
          },
          "hashtag_set": {
            "type": "string",
            "description": "Name of a saved hashtag set (see GET /hashtag-sets) to apply to this post, matched case-insensitively. The set's tags are merged in ONCE at create time: the post stores plain text, so later edits to the set never change this post. Tags already present in a caption are skipped (case-insensitive), and hard platform caps (Instagram 30) are enforced with a 400 `hashtag_limit_exceeded` before the platform can reject the post. Mutually interchangeable with `hashtag_set_id`.",
            "example": "Fitness Brand"
          },
          "hashtag_set_id": {
            "type": "string",
            "description": "Id of a saved hashtag set to apply. Same behavior as `hashtag_set`; pass one or the other.",
            "example": "57"
          },
          "hashtag_placement": {
            "type": "string",
            "enum": [
              "caption_append",
              "first_comment"
            ],
            "default": "caption_append",
            "description": "Where the set's tags land. `caption_append` (default) appends them to each target platform's caption after a blank line. `first_comment` puts them in the auto first comment on comment-capable platforms (Instagram, Facebook, LinkedIn Profile/Page, YouTube, TikTok with comments enabled, Telegram), appended after any `first_comment` you passed explicitly, and falls back to caption_append on platforms without a comment API. Stories always use captions (first comments are never posted on Stories)."
          },
          "hashtag_platforms": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "instagram"
            },
            "description": "Optional subset of the post's selected platforms to apply the set to (e.g. only [\"instagram\", \"tiktok\"]). Defaults to all selected platforms."
          },
          "approval_workflow_id": {
            "type": "string",
            "description": "Id of an approval workflow (see GET /approval-workflows) to route this post through. The post is created with status `in_approval` and `approval_status: pending` instead of `scheduled`: the workflow's first-step approvers are notified (dashboard badge, email, Slack), they review it in the dashboard's Approvals page, and it publishes at `schedule_at` only once the last step approves it (dashboard, or POST /posts/{id}/approve). A rejection leaves it `rejected`. Requires `schedule_at`; cannot be combined with `publish_now`. Errors: 404 `workflow_not_found` (not a workflow of this company/workspace), 400 `validation_error` (missing `schedule_at`, `publish_now` set, or the workflow has no approvers on its first step).",
            "example": "57823504"
          },
          "comment_dm_automation": {
            "type": "object",
            "nullable": true,
            "description": "A comment-to-DM automation bound to this post (Instagram must be in `accounts`). When somebody comments one of `trigger.keywords` on the post once it is live, the account DMs them `actions.dm.text`, once per person, and optionally posts one of `actions.public_reply.variations` under the comment. The shape is the `AutomationInput` of POST /automations without `post_id`, `scope` or `kinds` (always this post, comment trigger). The created automation comes back as `comment_dm_automation` on the post. Needs the opt-in `automations:write` scope on the API key (403 `insufficient_scope` without it).",
            "properties": {
              "trigger": {
                "type": "object",
                "properties": {
                  "keywords": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "guide"
                    }
                  },
                  "match": {
                    "type": "string",
                    "enum": [
                      "word",
                      "contains",
                      "any"
                    ]
                  }
                }
              },
              "actions": {
                "$ref": "#/components/schemas/AutomationActions"
              },
              "limits": {
                "$ref": "#/components/schemas/AutomationLimits"
              }
            },
            "example": {
              "trigger": {
                "keywords": [
                  "guide"
                ]
              },
              "actions": {
                "dm": {
                  "text": "Here is the guide: https://example.com/guide"
                },
                "public_reply": {
                  "enabled": true,
                  "variations": [
                    "Sent you a DM!"
                  ]
                }
              },
              "limits": {
                "max_sends": 500
              }
            }
          },
          "pinterest": {
            "type": "object",
            "description": "Pinterest-specific options. Attach 2–5 images via `media_urls.pinterest` (or the flat `media_urls`) to publish a single carousel pin; the title, description, link, and alt_text apply to the whole pin. A single image or a single video still publishes as a normal image / video pin. **Carousel constraints:** carousels hard-cap at 5 images: attaching more responds 400 `validation_error` at create/schedule time. Pinterest's v5 API also requires every slide in a carousel pin to share the same aspect ratio (1% tolerance). If you attach mixed-ratio images and try to schedule or publish, the API responds 400 `validation_error` with `mismatched_slides: [n, ...]` listing the 1-indexed slide numbers that need to be re-cropped or removed. Both checks run when `publish_now: true`, when `schedule_at` is set, and when `PATCH` flips a draft to a scheduled status. Drafts are exempt so you can iterate on media.",
            "properties": {
              "board_id": {
                "type": "string",
                "description": "Required. Pinterest board ID",
                "example": "1234567890123456789"
              },
              "title": {
                "type": "string",
                "description": "Pin title (max 100 characters). For carousel pins, applies to the whole pin, not individual slides.",
                "example": "Autumn menu"
              },
              "link": {
                "type": "string",
                "description": "Destination URL for the pin",
                "example": "https://example.com/menu"
              },
              "video_cover": {
                "type": "string",
                "description": "Cover image URL for video pins (a JPEG or PNG image URL)",
                "example": "https://example.com/cover.jpg"
              },
              "alt_text": {
                "type": "string",
                "description": "Accessibility alt text for the pin image (max 500 characters)",
                "example": "A bowl of pumpkin soup on a wooden table"
              },
              "product_tags": {
                "type": "array",
                "maxItems": 24,
                "items": {
                  "type": "string"
                },
                "description": "Products to tag on the Pin, so people can shop the items in the image. Up to 24 product Pins of the connected Pinterest account, each as a Pin ID string or a Pin link (`https://www.pinterest.com/pin/<id>/`); stored and returned as Pin IDs. Get the IDs from `GET /pinterest/products`, or check one with `GET /pinterest/products/validate`. Pinterest accepts a product only when its Pin is public, belongs to the same account and links to a website that account claimed; products of other merchants cannot be tagged. The tags are added right after the Pin is published. A product Pinterest refuses never fails the post: the other products are still tagged and the outcome is returned as `product_tags_result` on the post. An invalid entry or more than 24 products responds 400 `validation_error`. On `PATCH`, the `pinterest` object replaces the stored one, so leave `product_tags` out (or send `[]`) to remove the tags.",
                "example": [
                  "813744226420795884",
                  "813744226420795885"
                ]
              },
              "product_tags_result": {
                "type": "object",
                "readOnly": true,
                "description": "Read-only. Present on a published post that had `product_tags`: what Pinterest did with them.",
                "properties": {
                  "requested": {
                    "type": "integer",
                    "description": "Number of products that were sent.",
                    "example": 2
                  },
                  "tagged": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "813744226420795884"
                    },
                    "description": "Pin IDs that are tagged on the Pin."
                  },
                  "skipped": {
                    "type": "array",
                    "description": "Products that were not tagged, each with a reason.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "pin_id": {
                          "type": "string",
                          "example": "813744226420795885"
                        },
                        "reason": {
                          "type": "string",
                          "description": "Pinterest's reason code: `PIN_MISSING` (Pin not found), `PIN_IS_PRIVATE`, `PRODUCT_METADATA_MISSING` (not a product Pin), `PIN_NOT_FROM_VERIFIED_DOMAIN` (the product link is not on a claimed website), `PIN_NOT_FROM_SAME_USER_AS_HERO_PIN` (Pin of another account).",
                          "example": "PIN_IS_PRIVATE"
                        }
                      }
                    }
                  },
                  "error": {
                    "type": "string",
                    "nullable": true,
                    "description": "Set when the tag request itself failed (for example Pinterest was down); `tagged` is then empty."
                  }
                }
              }
            }
          },
          "youtube": {
            "type": "object",
            "description": "YouTube Shorts options. Only applies when `type` is `reel` and `youtube` is among the selected accounts.",
            "properties": {
              "title": {
                "type": "string",
                "description": "Short title shown on YouTube (max 100 characters). When omitted, the first line of the caption is used, cut at a word to 100 characters; \"YouTube Short\" only when there is no caption either.",
                "example": "How we plan a week of posts"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string",
                  "example": "social media"
                },
                "description": "Video tags"
              },
              "privacy_status": {
                "type": "string",
                "enum": [
                  "public",
                  "private",
                  "unlisted"
                ],
                "default": "public"
              },
              "category_id": {
                "type": "string",
                "description": "YouTube category ID (default \"22\" People & Blogs)",
                "example": "22"
              },
              "made_for_kids": {
                "type": "boolean",
                "default": false
              },
              "notify_subscribers": {
                "type": "boolean",
                "default": false
              },
              "contains_synthetic_media": {
                "type": "boolean",
                "default": false,
                "description": "Mark as AI-generated content"
              },
              "first_comment": {
                "type": "string",
                "maxLength": 10000,
                "description": "Text auto-posted as the first comment on the video right after it publishes (commentThreads.insert). The video must have comments enabled. Empty/omitted = no first comment.",
                "example": "Full menu at the link in our bio."
              }
            }
          },
          "instagram": {
            "type": "object",
            "description": "Instagram Reel options",
            "properties": {
              "share_to_feed": {
                "type": "boolean",
                "default": false,
                "description": "Also share reel to feed"
              },
              "thumbnail_type": {
                "type": "string",
                "enum": [
                  "from-video",
                  "from-library"
                ],
                "description": "How to select thumbnail"
              },
              "thumb_offset": {
                "type": "integer",
                "description": "Video timestamp in milliseconds (when thumbnail_type is from-video)",
                "example": 3000
              },
              "cover_url": {
                "type": "string",
                "description": "Custom thumbnail URL (when thumbnail_type is from-library)",
                "example": "https://example.com/cover.jpg"
              },
              "audio_id": {
                "type": "string",
                "description": "Licensed music for the Reel: an audio ID from `GET /audio/search`. Replaces/mixes with the video's own audio at publish time. Requires a Facebook account connected to the workspace whose Page is linked to this Instagram account. Reels only.",
                "example": "587784541076604"
              },
              "audio_volume": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "default": 100,
                "description": "Volume of the attached music track (0 = muted). Only used with audio_id."
              },
              "video_volume": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100,
                "default": 100,
                "description": "Volume of the video's original audio (0 = muted, e.g. music-only Reel). Only used with audio_id."
              },
              "first_comment": {
                "type": "string",
                "maxLength": 2200,
                "description": "Text auto-posted as the first comment on the post/reel right after it publishes. Common for keeping hashtags out of the caption. Not posted for Stories. Empty/omitted = no first comment.",
                "example": "Full menu at the link in our bio."
              },
              "is_trial_reel": {
                "type": "boolean",
                "default": false,
                "description": "Publish the reel as an Instagram Trial Reel: shown to non-followers first to test performance before (optionally) graduating to everyone. Reels only. Not available on every account: Instagram requires roughly 1,000+ followers and enables the feature per account (the account sees a 'Trial' toggle when creating a reel in the Instagram app). Ineligible accounts fail at publish time with a clear per-platform error."
              },
              "trial_graduation_strategy": {
                "type": "string",
                "enum": [
                  "MANUAL",
                  "SS_PERFORMANCE"
                ],
                "default": "MANUAL",
                "description": "How a Trial Reel graduates to all followers. MANUAL: you decide in the Instagram app. SS_PERFORMANCE: Instagram automatically shares it with followers if it performs well. Only used with is_trial_reel."
              },
              "is_ai_generated": {
                "type": "boolean",
                "default": false,
                "description": "Self-disclosure that this post's media is AI-generated. Adds Instagram's 'AI info' label. Applies to single images, videos, and Reels; for a carousel the label applies to the whole post, not individual slides. Cannot be added or removed after publish, so set it at post time. Not available for Stories."
              }
            }
          },
          "facebook": {
            "type": "object",
            "description": "Facebook options",
            "properties": {
              "first_comment": {
                "type": "string",
                "maxLength": 8000,
                "description": "Text auto-posted as the first comment on the post/reel right after it publishes. Only works on Page posts (the API cannot comment on personal-profile posts). Not posted for Stories. Empty/omitted = no first comment.",
                "example": "Full menu at the link in our bio."
              },
              "thumbnail_type": {
                "type": "string",
                "enum": [
                  "from-video",
                  "from-library"
                ],
                "description": "Video thumbnail for feed videos and reels: 'from-video' uses the frame at `thumb_offset`, 'from-library' uploads the image at `cover_url`. Same shape as the Instagram reel fields; stored as `video_cover.overrides.facebook` and applied after the video is live (a thumbnail failure never fails the post). On PATCH, `null` removes the Facebook override."
              },
              "thumb_offset": {
                "type": "integer",
                "description": "Frame timestamp in milliseconds (when thumbnail_type is from-video)",
                "example": 3000
              },
              "cover_url": {
                "type": "string",
                "format": "uri",
                "description": "Custom thumbnail image URL, JPEG or PNG up to 10 MB (when thumbnail_type is from-library)",
                "example": "https://example.com/cover.jpg"
              }
            }
          },
          "linkedin": {
            "type": "object",
            "description": "LinkedIn Profile options",
            "properties": {
              "first_comment": {
                "type": "string",
                "maxLength": 1250,
                "description": "Text auto-posted as the first comment on the profile post right after it publishes. Common for 'link in first comment' to avoid the in-caption link reach penalty. Empty/omitted = no first comment.",
                "example": "Full menu: https://example.com/menu"
              },
              "carousel_as_images": {
                "type": "boolean",
                "default": false,
                "description": "Multi-image style for the profile post. By default a post with 2+ images publishes as LinkedIn's swipeable PDF document carousel. Set true to publish the images as a plain multi-image gallery instead. Ignored for 0-1 images, video posts, and polls."
              },
              "document_source": {
                "type": "string",
                "enum": [
                  "original_pdf",
                  "slides"
                ],
                "default": "original_pdf",
                "description": "Where the profile post's document carousel comes from when its images are exactly the rendered pages of one uploaded PDF, in order and unchanged. `original_pdf` (default): LinkedIn receives the kept original file, so text stays sharp, in-document links work, viewers download the real file, and every page is included even past the 20-slide cap. `slides`: the document is rebuilt from the slide images instead (a flattened copy). Ignored when the images are not a PDF's pages, when `carousel_as_images` is true, for 0-1 images, video posts, and polls."
              }
            }
          },
          "linkedin_page": {
            "type": "object",
            "description": "LinkedIn Company Page options",
            "properties": {
              "first_comment": {
                "type": "string",
                "maxLength": 1250,
                "description": "Text auto-posted as the first comment on the company-page post right after it publishes. Empty/omitted = no first comment.",
                "example": "Full menu: https://example.com/menu"
              },
              "carousel_as_images": {
                "type": "boolean",
                "default": false,
                "description": "Multi-image style for the company-page post. By default a post with 2+ images publishes as LinkedIn's swipeable PDF document carousel. Set true to publish the images as a plain multi-image gallery instead. Ignored for 0-1 images, video posts, and polls."
              },
              "document_source": {
                "type": "string",
                "enum": [
                  "original_pdf",
                  "slides"
                ],
                "default": "original_pdf",
                "description": "Same as `linkedin.document_source`, for the company-page post: `original_pdf` (default) sends the kept original PDF when the images are its unchanged pages; `slides` rebuilds the document from the slide images."
              }
            }
          },
          "linkedin_poll": {
            "type": "object",
            "nullable": true,
            "description": "Non-sponsored LinkedIn poll(s), independent per channel, keyed by `linkedin` (personal profile) / `linkedin_page` (company page). A poll is mutually exclusive with media and a link share on that channel's post: a poll takes priority over both at publish time. Requires `content.linkedin` (or `content.default`) as that channel's caption/commentary; the poll itself only carries the question/options/duration.",
            "properties": {
              "linkedin": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LinkedInPollFields"
                  }
                ],
                "nullable": true,
                "description": "Poll for the personal profile post. Omit or null = not a poll."
              },
              "linkedin_page": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LinkedInPollFields"
                  }
                ],
                "nullable": true,
                "description": "Poll for the company page post. Omit or null = not a poll."
              }
            }
          },
          "tiktok": {
            "type": "object",
            "description": "TikTok options",
            "properties": {
              "title": {
                "type": "string",
                "maxLength": 90,
                "description": "Photo carousels only. The title TikTok shows above the caption on Photo Mode posts (max 90 characters). TikTok's photo endpoint takes the title and the caption as two separate fields; the caption comes from `content`. Ignored on video posts, which have a single caption field. Longer than 90 characters returns 400 validation_error. Read back as `tiktok.title` on GET /posts/{id}.",
                "example": "Autumn menu"
              },
              "privacy_level": {
                "type": "string",
                "enum": [
                  "PUBLIC_TO_EVERYONE",
                  "MUTUAL_FOLLOW_FRIENDS",
                  "FOLLOWER_OF_CREATOR",
                  "SELF_ONLY"
                ],
                "default": "PUBLIC_TO_EVERYONE"
              },
              "disable_comment": {
                "type": "boolean",
                "default": false
              },
              "disable_duet": {
                "type": "boolean",
                "default": false
              },
              "disable_stitch": {
                "type": "boolean",
                "default": false
              },
              "video_cover_timestamp_ms": {
                "type": "integer",
                "default": 0,
                "description": "Reels only. Timestamp (ms) of the video frame to use as the cover."
              },
              "is_aigc": {
                "type": "boolean",
                "default": false,
                "description": "Mark as AI-generated content"
              },
              "brand_content_toggle": {
                "type": "boolean",
                "default": false,
                "description": "Paid partnership disclosure (promotes a third-party brand)"
              },
              "brand_organic_toggle": {
                "type": "boolean",
                "default": false,
                "description": "Your own brand disclosure (promotes your own business)"
              },
              "auto_add_music": {
                "type": "boolean",
                "default": false,
                "description": "Photo carousels only. When true, TikTok auto-selects a soundtrack. Defaults to false to avoid unsuitable tracks."
              },
              "first_comment": {
                "type": "string",
                "maxLength": 150,
                "example": "Full menu at the link in our bio.",
                "description": "Text auto-posted as the first comment on the TikTok video right after it publishes (max 150 characters). Requires the workspace's TikTok comments authorization (the \"Enable comments\" button on the TikTok channel card, a second TikTok consent); without it `first_comment_result.status` is `failed` with an explanatory error. The video must be public with comments allowed. TikTok sometimes returns the final video id a few minutes after publish. In that case the comment is posted automatically as soon as the id resolves (`first_comment_result.pending` is true meanwhile). Empty/omitted = no first comment."
              }
            }
          },
          "x": {
            "type": "object",
            "description": "X (Twitter) options",
            "properties": {
              "reply_settings": {
                "type": "string",
                "enum": [
                  "",
                  "following",
                  "mentionedUsers"
                ],
                "description": "Who can reply (empty = everyone)"
              },
              "paid_partnership": {
                "type": "boolean",
                "default": false,
                "description": "Mark as a paid partnership disclosure"
              },
              "made_with_ai": {
                "type": "boolean",
                "default": false,
                "description": "Mark as AI-generated content"
              },
              "poll": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/XPoll"
                  }
                ],
                "nullable": true,
                "description": "Attach a poll to the tweet (the tweet text is the question). 2 to 4 options of up to 25 characters, open for `duration_minutes` (5 to 10080, 7 days; default 1440). A poll and media do not go together on X: the request is refused when the X channel also has media. On a thread the poll goes on the first tweet. On PATCH, `null` removes it."
              },
              "thread_parts": {
                "type": "array",
                "minItems": 2,
                "maxItems": 25,
                "description": "Publish as a chained thread instead of a single tweet. When omitted (or length < 2), `content.x` (or `content.default`) is used as a single tweet. Each part is posted in order via `in_reply_to_tweet_id`. Attach media to any part (first tweet or any reply) with either `media_ids` (numeric Library IDs from POST /v1/media, the same IDs single posts use) or `media_urls` (external links). Combined max is 4 media items per part. If the post also has top-level `media_urls.x` / media and the first part has no media of its own, the top-level media is attached to the first tweet.",
                "items": {
                  "type": "object",
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 25000,
                      "description": "Tweet text. Limit depends on the connected account's tier: 280 chars standard, 25,000 for X Premium/Premium+. Counted the way X counts: every link weighs 23 characters (its t.co length) regardless of the real URL length, and emoji/CJK/most symbols weigh 2."
                    },
                    "media_ids": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as numeric Library IDs returned by POST /v1/media (bare IDs or { id, alt } objects). Resolved to hosted URLs at create time. Combined with media_urls, max 4 per tweet."
                    },
                    "media_urls": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "required": [
                              "url"
                            ],
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as external URLs (downloaded + hosted), as URL strings or { url, alt } objects. Combined with media_ids, max 4 per tweet."
                    }
                  }
                },
                "example": [
                  {
                    "text": "1/3: our results are in",
                    "media_ids": [
                      "12345"
                    ]
                  },
                  {
                    "text": "2/3: grab your free pick at https://example.com/signup"
                  },
                  {
                    "text": "3/3: and here's the slip",
                    "media_ids": [
                      "12346"
                    ]
                  }
                ]
              }
            }
          },
          "bluesky": {
            "type": "object",
            "description": "Bluesky options",
            "properties": {
              "thread_parts": {
                "type": "array",
                "minItems": 2,
                "maxItems": 25,
                "description": "Publish as a chained Bluesky thread instead of a single post. When omitted (or length < 2), `content.bluesky` (or `content.default`) is used as a single post. Each part is posted in order via AT Protocol reply refs (root + parent) so it renders as a thread. Attach media to any part with either `media_ids` (numeric Library IDs from POST /v1/media, the same IDs single posts use) or `media_urls` (external links). A part is a single video OR up to 4 images. If the post also has top-level `media_urls.bluesky` / media and the first part has no media of its own, the top-level media is attached to the first post. Links, mentions and hashtags are made clickable automatically (rich-text facets).",
                "items": {
                  "type": "object",
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 300,
                      "description": "Post text (≤ 300 characters, counted as graphemes, so one emoji counts as 1)."
                    },
                    "media_ids": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as numeric Library IDs returned by POST /v1/media (bare IDs or { id, alt } objects). Resolved to hosted URLs at create time. A part is one video OR up to 4 images."
                    },
                    "media_urls": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "required": [
                              "url"
                            ],
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) for this media item."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as external URLs (downloaded + hosted), as URL strings or { url, alt } objects. A part is one video OR up to 4 images."
                    }
                  }
                },
                "example": [
                  {
                    "text": "1/3: our results are in"
                  },
                  {
                    "text": "2/3: read the full writeup at https://example.com/post"
                  },
                  {
                    "text": "3/3: thanks for reading! #buildinpublic"
                  }
                ]
              }
            }
          },
          "mastodon": {
            "type": "object",
            "description": "Mastodon options",
            "properties": {
              "poll": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/MastodonPoll"
                  }
                ],
                "nullable": true,
                "description": "Attach a poll to the status (the status text is the question). Options and `expires_in` (seconds) within the instance's limits: Mastodon's defaults are 2 to 4 options of up to 50 characters and 5 minutes to 1 month; `multiple` allows several answers, `hide_totals` hides the counts until the poll ends. A poll and media do not go together on Mastodon: the request is refused when the Mastodon channel also has media. On a thread the poll goes on the first status. On PATCH, `null` removes it."
              },
              "thread_parts": {
                "type": "array",
                "minItems": 2,
                "maxItems": 25,
                "description": "Publish as a chained Mastodon thread instead of a single status. When omitted (or length < 2), `content.mastodon` (or `content.default`) is used as a single status. Each part is posted in order as a native reply to the previous status (in_reply_to_id) so it renders as a thread. Attach media to any part with either `media_ids` (numeric Library IDs from POST /v1/media, the same IDs single posts use) or `media_urls` (external links). Max 4 media items per part. If the post also has top-level `media_urls.mastodon` / media and the first part has no media of its own, the top-level media is attached to the first status.",
                "items": {
                  "type": "object",
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 500,
                      "description": "Status text (≤ 500 characters by default; some instances allow more)."
                    },
                    "media_ids": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) shown to screen-reader users. The Mastodon community strongly values alt text on images."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as numeric Library IDs returned by POST /v1/media (bare IDs or { id, alt } objects). Resolved to hosted URLs at create time. Max 4 media items per part."
                    },
                    "media_urls": {
                      "type": "array",
                      "maxItems": 4,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "required": [
                              "url"
                            ],
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) shown to screen-reader users. The Mastodon community strongly values alt text on images."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as external URLs (downloaded + hosted), as URL strings or { url, alt } objects. Max 4 media items per part."
                    }
                  }
                },
                "example": [
                  {
                    "text": "1/3: our picks are in"
                  },
                  {
                    "text": "2/3: read the full writeup at https://example.com/post"
                  },
                  {
                    "text": "3/3: thanks for reading! #fediverse"
                  }
                ]
              }
            }
          },
          "telegram": {
            "type": "object",
            "description": "Telegram options (2026-10-17). Telegram posts go to the connected channel or group through the OmniSocials bot. Text up to 4,096 characters; with media, text over 1,024 characters is sent as a reply under the media (Telegram's caption limit). Media: up to 10 photos and videos (an album), one GIF, one video up to 50 MB, or files. No Stories. Views are not available to bots: analytics report reactions and subscribers.",
            "properties": {
              "silent": {
                "type": "boolean",
                "description": "Send without a notification sound.",
                "example": false
              },
              "link_preview": {
                "type": "boolean",
                "default": true,
                "description": "Show a preview of the first link of a text-only post. Set false to hide it."
              },
              "pin": {
                "type": "boolean",
                "example": true,
                "description": "Pin the post in the chat after it is sent. Needs the bot's Edit messages right in a channel (Pin messages in a group); without it the post is sent unpinned and `telegram.result.pin_error` says why."
              },
              "protect_content": {
                "type": "boolean",
                "description": "Stop forwarding and saving of the post in Telegram apps.",
                "example": false
              },
              "spoiler": {
                "type": "boolean",
                "description": "Blur the photos and videos until someone taps them.",
                "example": false
              },
              "caption_above_media": {
                "type": "boolean",
                "description": "Show the text above the media instead of below.",
                "example": false
              },
              "buttons": {
                "type": "array",
                "maxItems": 3,
                "items": {
                  "$ref": "#/components/schemas/TelegramButton"
                },
                "description": "Up to 3 link buttons under the post, one per row. Not on an album (2+ media) or a poll: the request is refused."
              },
              "poll": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/TelegramPoll"
                  }
                ],
                "nullable": true,
                "description": "Send a native Telegram poll; the post text is the question (up to 300 characters). No media with a poll. On PATCH, `null` removes it."
              },
              "first_comment": {
                "type": "string",
                "maxLength": 4096,
                "description": "A reply the bot sends under the post right after it is published.",
                "example": "Full menu at the link in our bio."
              }
            }
          },
          "threads": {
            "type": "object",
            "description": "Threads options: thread mode via `thread_parts`, location tag via `location_id`, poll via `poll`.",
            "properties": {
              "poll": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ThreadsPoll"
                  }
                ],
                "nullable": true,
                "description": "Attach a poll to the post (the post text is the question). 2 to 4 options of 1 to 25 characters; Threads closes the poll 24 hours after it is published (no duration parameter). Polls go on text-only posts: the request is refused when the Threads channel also has media. On a thread the poll goes on the first post. On PATCH, `null` removes it."
              },
              "location_id": {
                "type": "string",
                "nullable": true,
                "description": "Tag a location on the Threads post. Use an `id` returned by `GET /locations/search?platform=threads&q=...` (Threads location ids are NOT Facebook Place IDs, so do not reuse the Instagram `location_id` here). Sent to Threads as `location_id` on the container create call; on a multi-post thread (`thread_parts`) the tag goes on the first post. Requires the workspace's Threads connection to have the `threads_location_tagging` permission: create/update/publish return 400 `validation_error` naming the reconnect when it is missing, and 400 when location tagging is not enabled in this environment yet. On PATCH, `null` clears the tag. Alternative: pass `location` as an object `{ id, name, address, city, country }` to store display fields along with the id; `location_id` wins when both are given.",
                "example": "17841400000000000"
              },
              "location": {
                "type": "object",
                "nullable": true,
                "description": "Alternative to `location_id`: the full search result so the dashboard can show the place name without another Threads call. Only `id` is required; the other fields are stored as given (not resolved against Threads). `null` clears the tag on PATCH.",
                "properties": {
                  "id": {
                    "type": "string",
                    "example": "17841400000000000"
                  },
                  "name": {
                    "type": "string",
                    "example": "Griffith Observatory"
                  },
                  "address": {
                    "type": "string",
                    "example": "2800 E Observatory Rd"
                  },
                  "city": {
                    "type": "string",
                    "example": "Los Angeles"
                  },
                  "country": {
                    "type": "string",
                    "example": "United States"
                  }
                }
              },
              "thread_parts": {
                "type": "array",
                "minItems": 2,
                "maxItems": 25,
                "description": "Publish as a chained Threads thread instead of a single post. When omitted (or length < 2), `content.threads` (or `content.default`) is used as a single post. Each part is posted in order as a native reply to the previous post (reply_to_id) so it renders as a thread under the first post. Attach media to any part with either `media_ids` (numeric Library IDs from POST /v1/media, the same IDs single posts use) or `media_urls` (external links), max 10 media items per part (images and videos can be mixed; 2+ items publish as a carousel). If the post also has top-level `media_urls.threads` / media and the first part has no media of its own, the top-level media is attached to the first post.",
                "items": {
                  "type": "object",
                  "required": [
                    "text"
                  ],
                  "properties": {
                    "text": {
                      "type": "string",
                      "maxLength": 500,
                      "description": "Post text (up to 500 characters)."
                    },
                    "media_ids": {
                      "type": "array",
                      "maxItems": 10,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string"
                          },
                          {
                            "type": "object",
                            "required": [
                              "id"
                            ],
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) sent to Threads as alt_text."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as numeric Library IDs returned by POST /v1/media (bare IDs or { id, alt } objects). Resolved to hosted URLs at create time. Max 10 media items per part."
                    },
                    "media_urls": {
                      "type": "array",
                      "maxItems": 10,
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "format": "uri"
                          },
                          {
                            "type": "object",
                            "required": [
                              "url"
                            ],
                            "properties": {
                              "url": {
                                "type": "string",
                                "format": "uri"
                              },
                              "alt": {
                                "type": "string",
                                "maxLength": 1500,
                                "description": "Accessibility description (alt text) sent to Threads as alt_text."
                              }
                            }
                          }
                        ]
                      },
                      "description": "Optional per-part media as external URLs (downloaded + hosted), as URL strings or { url, alt } objects. Max 10 media items per part."
                    }
                  }
                },
                "example": [
                  {
                    "text": "1/3: our results are in"
                  },
                  {
                    "text": "2/3: read the full writeup: https://example.com/post"
                  },
                  {
                    "text": "3/3: thanks for reading! #buildinpublic"
                  }
                ]
              }
            }
          },
          "google_business": {
            "type": "object",
            "description": "Google Business Profile options. Use to publish EVENT or OFFER posts in addition to STANDARD, attach a call-to-action button, or both. STANDARD posts can also use this object purely to add a CTA. The shape mirrors Google's LocalPost JSONB exactly; see https://developers.google.com/my-business/reference/rest/v4/accounts.locations.localPosts#LocalPost.\n\nCaption rules, enforced at scheduling (text that violates these returns `validation_error` 400 before the post is saved):\n  • No phone numbers in the caption (use a CALL button instead).\n  • No inline URLs / bare domains / email addresses (use a LEARN_MORE, BOOK, SHOP, SIGN_UP, or ORDER button instead).\n  • Caption max 1500 characters.\n  • Media optional (text-only posts allowed). If attached: exactly one JPEG/PNG/WebP image (no video, no carousels).\n  • The workspace must have a Google Business location selected (Settings → Organisation → Workspaces → Google Business) before scheduling.\n",
            "properties": {
              "topic_type": {
                "type": "string",
                "enum": [
                  "STANDARD",
                  "EVENT",
                  "OFFER"
                ],
                "default": "STANDARD",
                "description": "Local post topic type. ALERT is reserved by Google and not exposed."
              },
              "cta": {
                "type": "object",
                "description": "Optional call-to-action button rendered under the caption on the Google Business Profile.",
                "properties": {
                  "actionType": {
                    "type": "string",
                    "enum": [
                      "LEARN_MORE",
                      "BOOK",
                      "ORDER",
                      "SHOP",
                      "SIGN_UP",
                      "CALL"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Required for every actionType except CALL. CALL uses the location's phone number from the business profile.",
                    "example": "https://example.com/book"
                  }
                }
              },
              "event": {
                "type": "object",
                "description": "Required when topic_type is EVENT or OFFER (Google requires the title and dates for offers too: the offer's name and validity period).",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 58,
                    "example": "Autumn tasting night"
                  },
                  "schedule": {
                    "type": "object",
                    "description": "Google's split date+time shape. startDate is required; startTime/endDate/endTime are optional. End (if present) must be ≥ start.",
                    "properties": {
                      "startDate": {
                        "type": "object",
                        "properties": {
                          "year": {
                            "type": "integer",
                            "example": 2026
                          },
                          "month": {
                            "type": "integer",
                            "example": 10
                          },
                          "day": {
                            "type": "integer",
                            "example": 24
                          }
                        }
                      },
                      "startTime": {
                        "type": "object",
                        "properties": {
                          "hours": {
                            "type": "integer",
                            "example": 18
                          },
                          "minutes": {
                            "type": "integer",
                            "example": 0
                          }
                        }
                      },
                      "endDate": {
                        "type": "object",
                        "properties": {
                          "year": {
                            "type": "integer",
                            "example": 2026
                          },
                          "month": {
                            "type": "integer",
                            "example": 10
                          },
                          "day": {
                            "type": "integer",
                            "example": 24
                          }
                        }
                      },
                      "endTime": {
                        "type": "object",
                        "properties": {
                          "hours": {
                            "type": "integer",
                            "example": 22
                          },
                          "minutes": {
                            "type": "integer",
                            "example": 0
                          }
                        }
                      }
                    }
                  }
                }
              },
              "offer": {
                "type": "object",
                "description": "Required when topic_type is OFFER. Must include at least one of couponCode or redeemOnlineUrl.",
                "properties": {
                  "couponCode": {
                    "type": "string",
                    "maxLength": 58,
                    "example": "AUTUMN20"
                  },
                  "redeemOnlineUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Must be https://. http URLs are rejected.",
                    "example": "https://example.com/offer"
                  },
                  "termsConditions": {
                    "type": "string",
                    "maxLength": 4000,
                    "example": "Valid until 31 October 2026."
                  }
                }
              }
            }
          }
        }
      },
      "Media": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Media ID (use this in post creation)",
            "example": "482913"
          },
          "url": {
            "type": "string",
            "description": "Permanent CDN URL for the media",
            "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-k3j9xq_0c9e6b1a-2f4d-4b8e-a7c3-9d1e5f6a2b70.jpg"
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true,
            "description": "CDN URL of the generated preview thumbnail (videos only; null for images, which use `url` directly)",
            "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/thumbnails/1792573200000-k3j9xq_0c9e6b1a-2f4d-4b8e-a7c3-9d1e5f6a2b70_thumbnail.webp"
          },
          "type": {
            "type": "string",
            "enum": [
              "image",
              "video",
              "document"
            ],
            "description": "`document` is a PDF kept as one item (see `pdf`): its id in `media_ids` expands into every page image, in order."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Human-readable label set on upload or via PATCH (e.g. \"pp-play5get50\"). Falls back to the storage filename when unset. Search this with the `search` query param.",
            "example": "autumn-menu-hero"
          },
          "folder_id": {
            "type": "string",
            "nullable": true,
            "description": "Id of the folder this file lives in, or null for the root (\"All media\")",
            "example": "12"
          },
          "filename": {
            "type": "string",
            "description": "Generated storage filename (stable, used internally)",
            "example": "0c9e6b1a-2f4d-4b8e-a7c3-9d1e5f6a2b70.jpg"
          },
          "size": {
            "type": "string",
            "description": "Human-formatted file size (e.g. \"2.50 MB\")",
            "example": "2.50 MB"
          },
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "processing",
              "failed"
            ],
            "description": "Processing state. 'ready' for normal uploads (usable immediately). Large videos uploaded via URL ingestion (over 100 MB) start as 'processing': poll get media / list media until 'ready' before using them in a post. 'failed' means the source couldn't be fetched or validated.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T14:32:10.000Z"
          },
          "pdf": {
            "type": "object",
            "description": "Present on a PDF document item (type `document`): the kept file and its rendered `pages` in order. Also present on a slide rendered from a PDF (type `image`): which original file and `page` it came from. The original PDF is kept in storage. When a LinkedIn post's images are exactly the rendered pages of one PDF, in order and unchanged, LinkedIn receives that original file (sharp text, working in-document links, the real download) instead of a document rebuilt from the page images. Opt out per channel with `document_source: \"slides\"` on the post's `linkedin` / `linkedin_page` options.\n",
            "properties": {
              "id": {
                "type": "string",
                "description": "Shared by the document item and every slide split from it.",
                "example": "9a7e3c1f-5b2d-4e8a-b6c4-1f3d5e7a9b20"
              },
              "name": {
                "type": "string",
                "description": "Original file name, e.g. \"deck.pdf\".",
                "example": "deck.pdf"
              },
              "url": {
                "type": "string",
                "description": "CDN URL of the original PDF.",
                "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-p7m2wd_9a7e3c1f-5b2d-4e8a-b6c4-1f3d5e7a9b20.pdf"
              },
              "page": {
                "type": "integer",
                "description": "Slides only. 1-based page this slide renders.",
                "example": 1
              },
              "total_pages": {
                "type": "integer",
                "example": 8
              },
              "rendered_pages": {
                "type": "integer",
                "description": "Pages turned into page images (max 20).",
                "example": 8
              },
              "truncated": {
                "type": "boolean",
                "description": "Document items only. True when the file has more pages than were rendered.",
                "example": false
              },
              "pages": {
                "type": "array",
                "description": "Document items only. The rendered page images, in page order; these are what a post carries.",
                "items": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-p7m2wd_4c2e8a6f-1d3b-4f5a-9e7c-0b2d4f6a8c13.png"
                    },
                    "width": {
                      "type": "integer",
                      "nullable": true,
                      "example": 1080
                    },
                    "height": {
                      "type": "integer",
                      "nullable": true,
                      "example": 1350
                    }
                  }
                }
              }
            }
          }
        }
      },
      "MediaCompatibility": {
        "type": "object",
        "description": "Whether the file fits the platforms connected to your workspace. `compatible` is true when no connected platform would reject it.\n",
        "properties": {
          "compatible": {
            "type": "boolean",
            "example": false
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "platform": {
                  "type": "string",
                  "example": "instagram"
                },
                "display_name": {
                  "type": "string",
                  "example": "Instagram"
                },
                "reasons": {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "example": "max 300 MB"
                  },
                  "description": "e.g. [\"max 300 MB\"], [\"format not supported\"]"
                }
              }
            }
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "One-line human summary, or null when fully compatible.",
            "example": "This file (450 MB) won't post to: Instagram (max 300 MB)."
          }
        }
      },
      "PdfUploadResult": {
        "type": "object",
        "description": "Returned when the uploaded file is a PDF. With `pdf_mode: slides` (the default) the server rasterizes each page into an image slide and stores each as its own media item, and keeps the original PDF too. `data` mirrors the FIRST slide (so callers that read a single Media still work); `slides` and `media_ids` carry the whole carousel in page order. Pass ALL of `media_ids` to POST /posts as a carousel. With `pdf_mode: document` the response has no `slides`: `data` is the single document item (type `document`, pages in `data.pdf.pages`) and `media_ids` holds its one id, which expands into every page at post time. On LinkedIn it posts as a swipeable document made from the original PDF file (text stays sharp, links keep working, viewers download the real file); on Instagram, TikTok, Threads and Pinterest as an image carousel. Slides are capped at 20 pages; the original sent to LinkedIn is the whole file.\n",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Media"
          },
          "slides": {
            "type": "array",
            "description": "Every page as a Media item, in page order.",
            "items": {
              "$ref": "#/components/schemas/Media"
            }
          },
          "media_ids": {
            "type": "array",
            "description": "Slide ids in page order. Pass all of them to POST /posts.",
            "items": {
              "type": "string",
              "example": "482913"
            }
          },
          "pdf": {
            "type": "object",
            "properties": {
              "total_pages": {
                "type": "integer",
                "example": 8
              },
              "rendered_pages": {
                "type": "integer",
                "example": 8
              },
              "truncated": {
                "type": "boolean",
                "example": false,
                "description": "True when the PDF had more than 20 pages and only the first 20 became slides. LinkedIn still receives the full original."
              },
              "id": {
                "type": "string",
                "description": "Id of the kept original PDF (shared by every slide's `pdf.id`).",
                "example": "9a7e3c1f-5b2d-4e8a-b6c4-1f3d5e7a9b20"
              },
              "name": {
                "type": "string",
                "description": "Original file name, e.g. \"deck.pdf\".",
                "example": "deck.pdf"
              },
              "url": {
                "type": "string",
                "description": "CDN URL of the kept original PDF.",
                "example": "https://cdn.omnisocials.com/file/library-omnisocials/312a658f-2b45-4289-b240-890807221a74/library/1792573200000-p7m2wd_9a7e3c1f-5b2d-4e8a-b6c4-1f3d5e7a9b20.pdf"
              },
              "size_bytes": {
                "type": "integer",
                "example": 2483017
              }
            }
          },
          "compatibility": {
            "$ref": "#/components/schemas/MediaCompatibility"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Folder": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parent_id": {
            "type": "string",
            "nullable": true,
            "description": "Parent folder id, or null for a top-level folder"
          },
          "item_count": {
            "type": "integer",
            "description": "Number of media items directly in this folder"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "HashtagSet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Set name, unique per workspace (case-insensitive)"
          },
          "hashtags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ordered tags WITHOUT the leading '#'"
          },
          "hashtag_count": {
            "type": "integer"
          },
          "preview": {
            "type": "string",
            "description": "The tags rendered as caption text, e.g. \"#fitness"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ApprovalWorkflowApprover": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "User id of the approver"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ApprovalWorkflowStep": {
        "type": "object",
        "properties": {
          "order": {
            "type": "integer",
            "description": "1-based step order; steps are approved in order"
          },
          "name": {
            "type": "string"
          },
          "require_mode": {
            "type": "string",
            "enum": [
              "any",
              "all"
            ],
            "description": "`any` = one approver of this step is enough; `all` = every approver must approve"
          },
          "approvers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApprovalWorkflowApprover"
            }
          }
        }
      },
      "ApprovalWorkflow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "57823504",
            "description": "Pass this as `approval_workflow_id` on POST /posts/create"
          },
          "name": {
            "type": "string",
            "example": "Client review"
          },
          "workspace_id": {
            "type": "integer",
            "nullable": true,
            "description": "Workspace the workflow is bound to, or null when it is available to every workspace of the company"
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApprovalWorkflowStep"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Account ID (use this when creating posts)",
            "example": "1234_instagram"
          },
          "platform": {
            "type": "string",
            "enum": [
              "instagram",
              "facebook",
              "linkedin",
              "linkedin_page",
              "youtube",
              "tiktok",
              "pinterest",
              "bluesky",
              "threads",
              "mastodon",
              "x",
              "google_business",
              "reddit",
              "snapchat",
              "telegram"
            ]
          },
          "username": {
            "type": "string",
            "description": "Social media username/handle",
            "example": "yourbrand"
          },
          "display_name": {
            "type": "string",
            "description": "Display name on the platform",
            "example": "Your Brand"
          },
          "profile_picture": {
            "type": "string",
            "description": "Profile picture URL",
            "example": "https://example.com/avatar.jpg"
          },
          "content_types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "post",
                "story",
                "reel"
              ]
            },
            "description": "Supported content types for this platform"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "needs_reconnect"
            ],
            "description": "Connection status. `needs_reconnect` means the account is still wired up to the workspace but its OAuth token has been revoked or expired; posts to this account will fail until the user reconnects in the OmniSocials UI. Most often caused by the user changing their password, deauthorizing OmniSocials on the platform, or the token expiring without a successful refresh."
          },
          "needs_reconnect": {
            "type": "boolean",
            "description": "Convenience boolean: `true` when `status` is `needs_reconnect`.",
            "example": false
          },
          "reauth_reason": {
            "type": "string",
            "nullable": true,
            "example": "OAuthException: Session has expired",
            "description": "Short reason from the platform (e.g. \"OAuthException: Session has expired\"). Present only when `needs_reconnect` is true. Token-health tracking exists for Instagram, Facebook, Threads, X, Pinterest, LinkedIn (profile + page); other platforms always report `needs_reconnect: false`."
          },
          "boards": {
            "type": "array",
            "description": "Pinterest boards (only present for Pinterest accounts). Use board `id` as `board_id` when creating Pinterest posts.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Board ID (use as board_id when creating posts)",
                  "example": "1234567890123456789"
                },
                "name": {
                  "type": "string",
                  "description": "Board name",
                  "example": "Recipes"
                }
              }
            }
          },
          "platform_details": {
            "type": "object",
            "description": "Platform-specific details. Present for X accounts with a Premium subscription, and for every Telegram account.",
            "properties": {
              "subscription_type": {
                "type": "string",
                "description": "X subscription tier (e.g. \"Premium\", \"PremiumPlus\")",
                "example": "Premium"
              },
              "chat_type": {
                "type": "string",
                "enum": [
                  "channel",
                  "group",
                  "supergroup"
                ],
                "description": "Telegram: what kind of chat OmniSocials posts to. In a group, posts show the OmniSocials bot as the sender."
              },
              "chat_id": {
                "type": "string",
                "description": "Telegram: the chat id (e.g. -1001234567890).",
                "example": "-1001234567890"
              },
              "subscribers": {
                "type": "integer",
                "nullable": true,
                "description": "Telegram: subscribers (channel) or members (group) at the last daily check.",
                "example": 1250
              },
              "url": {
                "type": "string",
                "nullable": true,
                "description": "Telegram: t.me link to the chat (public chats), or the t.me/c link members can open.",
                "example": "https://t.me/brand"
              }
            }
          },
          "connected_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-03-02T10:15:00.000Z"
          }
        }
      },
      "PostAnalytics": {
        "type": "object",
        "properties": {
          "post_id": {
            "type": "string",
            "example": "1024"
          },
          "platforms": {
            "type": "object",
            "description": "One entry per platform the post was published to. For thread platforms (X, Bluesky, Mastodon, Threads) a post can publish as a chain of several parts; the entry sums each part's metrics so you get the thread's total (matching the in-app analytics), and reports the thread's root as `platform_post_id`.\n",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "platform": {
                  "type": "string",
                  "example": "instagram"
                },
                "platform_post_id": {
                  "type": "string",
                  "description": "The post's id on the platform. For a thread this is the root part (tweet/post/toot).",
                  "example": "17912345678901234"
                },
                "metrics": {
                  "type": "object",
                  "description": "Platform-specific metrics, as the platform reports them. A missing key means the platform does not report it; a 0 is a measured zero. For a thread, counts are summed across all parts and rates/averages are averaged. Common keys: `views`, `impressions`, `reach`, `likes`, `comments`, `replies`, `shares`, `reposts`, `quotes`, `saves`, `bookmarks`, `clicks`, `link_clicks`, `profile_visits`, `follows` (follows gained from the post), `engagement`, `reactions` (object by type). `link_clicks` is clicks on links in the post (X `url_link_clicks` for posts under 30 days, LinkedIn profile LINK_CLICKS, Facebook link clicks from `clicks_breakdown`, Pinterest outbound clicks, Instagram story link sticker taps); `clicks` is the platform's broadest click count (Facebook `post_clicks`, LinkedIn Page `clickCount`, which cannot be split into link clicks). `reach` is absent, never 0, on platforms with no reach metric (X, Pinterest, YouTube, Threads, Bluesky, Mastodon) and on Facebook: Meta retired Facebook post reach in June 2026 and its replacement metric returns no data. Video keys: `video_views`, `duration` (s), `avg_watch_time` (s), `total_watch_time` (s), `watch_time_percentage`, `completion_rate`, `skip_rate`, `replays`, `engaged_views`, `traffic_sources` (YouTube, object), `impression_sources` and `audience_types` (TikTok Business, objects with percentages). Pinterest: `pin_clicks`, `outbound_clicks`, `save_rate`, `pin_click_rate`, `outbound_click_rate`, `video_starts`. Instagram stories: `navigation`, `navigation_breakdown`, `profile_activity_breakdown`, `completion_analysis`. Items a platform cannot measure (Google Business posts, deleted videos) carry `metrics_unavailable: true` and a `note` instead of zeros.\n",
                  "example": {
                    "views": 1520,
                    "reach": 1210,
                    "likes": 84,
                    "comments": 6,
                    "shares": 3,
                    "saves": 11
                  }
                },
                "thread_parts": {
                  "type": "integer",
                  "description": "Number of parts summed into this entry. 1 for a single (non-thread, single-slide) post. For a multi-slide story this is the number of slides with stats.",
                  "example": 1
                },
                "story_slides": {
                  "type": "array",
                  "description": "Multi-slide stories only. One entry per slide in publish order, each with that slide's own metrics (the parent `metrics` is the sum across slides). Absent on single-slide stories and on every other post type.\n",
                  "items": {
                    "type": "object",
                    "properties": {
                      "index": {
                        "type": "integer",
                        "description": "Zero-based slide position."
                      },
                      "platform_post_id": {
                        "type": "string",
                        "description": "The slide's own story id on the platform."
                      },
                      "metrics": {
                        "type": "object"
                      },
                      "collected_at": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  }
                },
                "collected_at": {
                  "type": "string",
                  "format": "date-time",
                  "description": "Most recent collection time across the summed parts.",
                  "example": "2026-10-21T15:00:12.000Z"
                }
              }
            }
          }
        }
      },
      "AnalyticsOverview": {
        "type": "object",
        "properties": {
          "total_posts": {
            "type": "integer",
            "example": 42
          },
          "total_platforms": {
            "type": "integer",
            "example": 4
          },
          "total_engagement": {
            "type": "number",
            "example": 3180
          },
          "total_impressions": {
            "type": "number",
            "example": 96400
          },
          "average_engagement_rate": {
            "type": "number",
            "description": "Engagement rate as a percentage (0-100)",
            "example": 3.3
          },
          "top_performing_platform": {
            "type": "string",
            "nullable": true,
            "example": "instagram"
          },
          "platform_breakdown": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "posts": {
                  "type": "integer",
                  "example": 12
                },
                "total_engagement": {
                  "type": "number",
                  "example": 1420
                },
                "total_impressions": {
                  "type": "number",
                  "example": 38200
                },
                "average_engagement": {
                  "type": "number",
                  "example": 118.3
                },
                "engagement_rate": {
                  "type": "number",
                  "example": 3.72
                }
              }
            }
          }
        }
      },
      "AccountAnalytics": {
        "type": "object",
        "properties": {
          "platform": {
            "type": "string",
            "example": "instagram"
          },
          "platform_account_id": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-10-20"
          },
          "metrics": {
            "type": "object",
            "description": "Account-level metrics for the snapshot date, as the platform reports them. Stock values: `followers`, `following`, `posts`, and platform extras (`monthly_views`, `pin_count`, `listed`, `media_count`, `average_rating`, `review_count`, `total_views`, `total_videos`). Day values (`period: \"daily\"`): `impressions` (views), `reach`, `engagement`, `likes`, `comments`, `shares`, `saves`, `profile_views`, `link_clicks`, `follows_gained`, `follows_lost`, `accounts_engaged`, `video_views`, `watch_time_minutes`, and Google Business `calls`, `direction_requests`, `website_clicks`, `bookings`, `impressions_breakdown`, `search_keywords`. Audience objects when available: `demographics` (age, gender, country, city, or LinkedIn function, seniority, industry) with `demographics_unit`, `online_followers` (UTC hour -> followers online), `link_clicks_breakdown` (Threads, per URL). LinkedIn profile rows keep `impressions_lifetime` and carry `period: \"lifetime\"` when the daily breakdown is not available. Read the `note` field where present.\n",
            "example": {
              "followers": 12840,
              "following": 310,
              "posts": 412
            }
          },
          "collected_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T23:10:04.000Z"
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "74327072-ec54-473c-837f-63b29187ac16"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL to receive webhook events",
            "example": "https://example.com/webhooks/omnisocials"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "post.scheduled",
                "post.published",
                "post.failed",
                "post.approved",
                "post.rejected"
              ]
            },
            "description": "Events to subscribe to"
          },
          "is_active": {
            "type": "boolean",
            "example": true
          },
          "last_triggered_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-21T09:00:07.000Z"
          },
          "failure_count": {
            "type": "integer",
            "description": "Consecutive delivery failures since the last success. Resets to 0 on any successful delivery, so 0 means currently healthy.",
            "example": 0
          },
          "last_failure": {
            "type": "object",
            "nullable": true,
            "description": "Details of the most recent failed delivery. Not cleared on success, so it persists as the last known problem even when failure_count is 0. Null if no delivery has ever failed.",
            "properties": {
              "at": {
                "type": "string",
                "format": "date-time",
                "description": "When the failure was recorded",
                "example": "2026-10-18T07:42:31.000Z"
              },
              "status": {
                "type": "integer",
                "nullable": true,
                "description": "HTTP status your endpoint returned, or null for a network error/timeout",
                "example": 404
              },
              "error": {
                "type": "string",
                "description": "Human-readable reason (e.g. \"Endpoint responded with HTTP 404\", \"No response within 10s (timeout)\")",
                "example": "Endpoint responded with HTTP 404"
              },
              "attempts": {
                "type": "integer",
                "description": "Number of delivery attempts made before giving up",
                "example": 4
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-09-30T12:00:00.000Z"
          }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique delivery event ID (also sent as the X-OmniSocials-Delivery header)",
            "example": "80fbb5db-f0fa-4555-8c8d-8fc4fde94468"
          },
          "type": {
            "type": "string",
            "enum": [
              "post.scheduled",
              "post.published",
              "post.failed",
              "post.approved",
              "post.rejected"
            ],
            "description": "Event type",
            "example": "post.published"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-21T09:00:07.000Z"
          },
          "data": {
            "type": "object",
            "description": "Event payload",
            "properties": {
              "post_id": {
                "type": "string",
                "description": "The post's ID (numeric string)",
                "example": "1024"
              },
              "workspace_id": {
                "type": "integer",
                "example": 1234
              },
              "status": {
                "type": "string",
                "description": "Post status at the time of the event (scheduled, posted, warning, failed, posting, rejected)",
                "example": "posted"
              },
              "post_type": {
                "type": "string",
                "example": "Post"
              },
              "approval": {
                "type": "object",
                "description": "Only on `post.approved` and `post.rejected`: how the review of the post ended. Read the full review (steps, approvers, comments) with GET /posts/{id}/approval.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "rejected"
                    ],
                    "example": "rejected"
                  },
                  "decided_by": {
                    "type": "string",
                    "description": "User ID of the approver who gave the last approval, or who rejected the post",
                    "example": "312a658f-2b45-4289-b240-890807221a74"
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "description": "The reason the approver gave with a rejection. Null on post.approved and when no reason was given.",
                    "example": "Wrong product photo"
                  }
                }
              },
              "scheduled_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-10-21T09:00:00.000Z"
              },
              "published_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-10-21T09:00:07.000Z"
              },
              "targets": {
                "type": "array",
                "description": "Per-platform results. Empty for post.scheduled, post.approved and post.rejected.",
                "items": {
                  "type": "object",
                  "properties": {
                    "platform": {
                      "type": "string",
                      "example": "instagram"
                    },
                    "status": {
                      "type": "string",
                      "description": "success or failed",
                      "example": "success"
                    },
                    "native_post_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "The post's ID on the platform itself (e.g. Instagram media ID, tweet ID). Null if the platform returned none.",
                      "example": "17912345678901234"
                    },
                    "error": {
                      "type": "string",
                      "description": "Present only on failed targets"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "PinterestProduct": {
        "type": "object",
        "properties": {
          "pin_id": {
            "type": "string",
            "description": "Use this in `pinterest.product_tags`.",
            "example": "813744226420795884"
          },
          "title": {
            "type": "string",
            "nullable": true,
            "example": "Blue ribbed top"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "link": {
            "type": "string",
            "nullable": true,
            "description": "The product page the Pin links to.",
            "example": "https://shop.example.com/products/blue-ribbed-top"
          },
          "image_url": {
            "type": "string",
            "nullable": true,
            "description": "A small image of the product Pin."
          },
          "price": {
            "type": "number",
            "nullable": true,
            "description": "Catalog source only.",
            "example": 24.99
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "Catalog source only. ISO 4217 code.",
            "example": "EUR"
          },
          "availability": {
            "type": "string",
            "nullable": true,
            "description": "Catalog source only, as Pinterest reports it (for example `IN_STOCK`, `OUT_OF_STOCK`, `PREORDER`)."
          },
          "item_id": {
            "type": "string",
            "nullable": true,
            "description": "Catalog source only. The merchant's own product id.",
            "example": "TOP-BLUE-M"
          }
        }
      },
      "InboxParticipant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Platform-scoped id of the other party"
          },
          "name": {
            "type": "string",
            "example": "Jamie Rivera"
          },
          "username": {
            "type": "string",
            "example": "jamierivera"
          },
          "profile_picture": {
            "type": "string",
            "nullable": true,
            "example": "https://example.com/avatar.jpg"
          }
        }
      },
      "InboxConversation": {
        "type": "object",
        "properties": {
          "conversation_id": {
            "type": "string",
            "example": "instagram_comment_17912345678901234"
          },
          "platform": {
            "type": "string",
            "enum": [
              "instagram",
              "facebook",
              "linkedin",
              "tiktok",
              "youtube",
              "x",
              "threads"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "dm",
              "comment",
              "mention"
            ],
            "example": "comment",
            "description": "dm = direct message; comment/mention = public interaction. LinkedIn is comment/mention only; TikTok and YouTube are comment only; X is dm only; Threads is comment (replies on your posts) and mention only."
          },
          "participant": {
            "$ref": "#/components/schemas/InboxParticipant"
          },
          "unread_count": {
            "type": "integer",
            "description": "Unread incoming messages in this conversation",
            "example": 1
          },
          "last_message": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "example": "88231"
              },
              "direction": {
                "type": "string",
                "enum": [
                  "incoming",
                  "outgoing"
                ]
              },
              "text": {
                "type": "string",
                "example": "What size is this?"
              },
              "timestamp": {
                "type": "string",
                "format": "date-time",
                "example": "2026-10-21T09:00:00.000Z"
              },
              "is_read": {
                "type": "boolean",
                "example": false
              }
            }
          },
          "post": {
            "$ref": "#/components/schemas/InboxPostContext"
          }
        }
      },
      "InboxPostContext": {
        "type": "object",
        "nullable": true,
        "description": "The post a comment/mention conversation belongs to, so a reply can be drafted with the post in view (\"what size is this?\" needs the caption and the picture). Null for DMs. `url` is the public link to the post when the platform provides one (Instagram, Facebook, YouTube, TikTok, LinkedIn, Threads); `media_type` is the platform's label (e.g. IMAGE, VIDEO, CAROUSEL_ALBUM on Instagram) when known.\n",
        "properties": {
          "id": {
            "type": "string",
            "nullable": true,
            "example": "17912345678901234"
          },
          "caption": {
            "type": "string",
            "nullable": true,
            "example": "Our autumn menu is here. #autumnmenu"
          },
          "thumbnail": {
            "type": "string",
            "nullable": true,
            "description": "Image URL of the post (or the video's cover)",
            "example": "https://example.com/photo.jpg"
          },
          "url": {
            "type": "string",
            "nullable": true,
            "example": "https://www.instagram.com/p/Cxyz123ABC/"
          },
          "media_type": {
            "type": "string",
            "nullable": true,
            "example": "IMAGE"
          }
        }
      },
      "InboxMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "88231"
          },
          "conversation_id": {
            "type": "string",
            "example": "instagram_comment_17912345678901234"
          },
          "platform": {
            "type": "string",
            "example": "instagram"
          },
          "type": {
            "type": "string",
            "enum": [
              "dm",
              "comment",
              "mention"
            ],
            "example": "comment"
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ]
          },
          "text": {
            "type": "string",
            "example": "What size is this?"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-21T09:00:00.000Z"
          },
          "is_read": {
            "type": "boolean",
            "example": false
          },
          "is_replied": {
            "type": "boolean",
            "example": false
          },
          "reaction": {
            "type": "string",
            "nullable": true
          },
          "parent_comment_id": {
            "type": "string",
            "nullable": true
          },
          "hidden": {
            "type": "boolean",
            "nullable": true,
            "example": false,
            "description": "Comments only: true when the comment is hidden on the platform (see POST /inbox/messages/{id}/hide). Null for DMs."
          },
          "permalink": {
            "type": "string",
            "nullable": true,
            "description": "Link to the reply or mentioning post on the platform, when known"
          },
          "attachment": {
            "type": "object",
            "nullable": true,
            "description": "Media sent with this message, when present. Incoming Instagram/Facebook DM images, videos, voice messages, and story mentions are re-hosted on our CDN so the URL stays valid indefinitely (the platform's own URL is short-lived). `type` reflects the fetched bytes, not the platform's label: a photo the platform sent under an unlabelled attachment type is `image`, a story mention that is really an MP4 is `video`. `type: file` covers media we could fetch but could not classify. `type: link` means the platform's URL did not point at media (for example a shared post that resolves to an instagram.com page); `url` is then the platform's own link, not re-hosted, and may require login or expire.\n",
            "properties": {
              "url": {
                "type": "string",
                "example": "https://example.com/photo.jpg"
              },
              "type": {
                "type": "string",
                "enum": [
                  "image",
                  "video",
                  "audio",
                  "file",
                  "link"
                ]
              }
            }
          },
          "sender": {
            "$ref": "#/components/schemas/InboxParticipant"
          },
          "post": {
            "$ref": "#/components/schemas/InboxPostContext"
          }
        }
      },
      "InboxNextUnanswered": {
        "type": "object",
        "nullable": true,
        "description": "The next conversation that needs an answer, with everything needed to draft the reply. Null when the queue is empty.",
        "properties": {
          "conversation": {
            "$ref": "#/components/schemas/InboxConversation"
          },
          "message": {
            "allOf": [
              {
                "$ref": "#/components/schemas/InboxMessage"
              }
            ],
            "description": "The unanswered incoming message itself: the customer's latest DM, or the specific comment. Its `conversation_id` is what the reply endpoint takes, and its `id` is the `message_id` to pass along with the reply for comment threads (so the reply lands under this comment and not under the newest one on the post); the same `id` is what POST /inbox/messages/{id}/hide and DELETE /inbox/messages/{id} take."
          },
          "messages": {
            "type": "array",
            "description": "The conversation so far, oldest first (the most recent 50 messages for long DM threads).",
            "items": {
              "$ref": "#/components/schemas/InboxMessage"
            }
          },
          "reply_window": {
            "type": "object",
            "description": "Whether a reply can still be sent through the API. Only Instagram and Facebook DMs have a window (Meta: 24 hours after the customer's last message); every other item has `open: true` and `closes_at: null`. When `open` is false the item is still served (the customer is still waiting) but the reply endpoint answers 422 `outside_messaging_window`: answer it from the native app (that reply is mirrored into the inbox and clears the item) or mark the conversation read to skip it.",
            "properties": {
              "open": {
                "type": "boolean",
                "example": true
              },
              "closes_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "example": "2026-10-22T09:00:00.000Z",
                "description": "When the window closes or closed (customer's last message + 24 h); null when there is no window."
              }
            }
          }
        }
      },
      "InboxCursorPagination": {
        "type": "object",
        "properties": {
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Pass as ?cursor= to fetch the next page; null when there are no more results",
            "example": "MjAyNi0xMC0yMVQwOTowMDowMC4wMDBafDg4MjMx"
          },
          "has_more": {
            "type": "boolean",
            "example": true
          },
          "limit": {
            "type": "integer",
            "example": 25
          }
        }
      },
      "AutomationTrigger": {
        "type": "object",
        "properties": {
          "kinds": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "comment",
                "dm",
                "story_reply"
              ]
            },
            "description": "What starts the automation. `comment` = a comment on an Instagram post or reel (needs `scope`), `dm` = a keyword in a DM to the account, `story_reply` = a keyword in a reply to a story. One automation can listen to several. Default `[\"comment\"]`."
          },
          "scope": {
            "type": "string",
            "enum": [
              "post",
              "next_post",
              "any"
            ],
            "description": "For the `comment` kind: `post` = one post (pass `post_id` or `instagram_media_id` on the request), `next_post` = the next Instagram post the workspace publishes, `any` = every post of the account. Default `post`."
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "guide"
            },
            "description": "Up to 20 keywords (60 characters each), matched case-insensitively. Required unless `match` is `any`."
          },
          "match": {
            "type": "string",
            "enum": [
              "word",
              "contains",
              "any"
            ],
            "description": "`word` (default) = the keyword stands on its own (\"guide\", \"Guide!\" match; \"linkedin\" does not match \"link\"), `contains` = the keyword appears anywhere, `any` = every comment or message with text."
          }
        }
      },
      "AutomationActions": {
        "type": "object",
        "required": [
          "dm"
        ],
        "properties": {
          "dm": {
            "type": "object",
            "required": [
              "text"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "The DM sent to the person (max 1000 characters). Sent once per person per automation.",
                "example": "Here is the guide: https://example.com/guide"
              }
            }
          },
          "public_reply": {
            "type": "object",
            "description": "A public reply under the comment, only for the `comment` kind. One of `variations` is picked at random per comment.",
            "properties": {
              "enabled": {
                "type": "boolean",
                "example": true
              },
              "variations": {
                "type": "array",
                "items": {
                  "type": "string",
                  "example": "Sent you a DM!"
                },
                "description": "Up to 10 replies of 300 characters."
              }
            }
          }
        }
      },
      "AutomationLimits": {
        "type": "object",
        "properties": {
          "max_sends": {
            "type": "integer",
            "nullable": true,
            "description": "Stop after this many DMs (the automation becomes `completed`). Omit for no limit.",
            "example": 500
          },
          "stop_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Stop at this time. Omit for no end.",
            "example": "2026-11-30T23:00:00.000Z"
          }
        }
      },
      "AutomationStats": {
        "type": "object",
        "properties": {
          "matched": {
            "type": "integer",
            "example": 120
          },
          "sent": {
            "type": "integer",
            "example": 112
          },
          "skipped": {
            "type": "integer",
            "example": 6
          },
          "failed": {
            "type": "integer",
            "example": 2
          },
          "last_event_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-10-21T15:00:12.000Z"
          }
        }
      },
      "Automation": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "318"
          },
          "type": {
            "type": "string",
            "enum": [
              "comment_to_dm"
            ]
          },
          "platform": {
            "type": "string",
            "enum": [
              "instagram"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "completed"
            ],
            "description": "`completed` = `max_sends` or `stop_at` was reached. A new limit or trigger starts it again."
          },
          "name": {
            "type": "string",
            "nullable": true,
            "example": "Guide DM"
          },
          "post_id": {
            "type": "string",
            "nullable": true,
            "description": "The OmniSocials post the automation listens on, for the `post` scope. `null` for a post published outside OmniSocials and for the other scopes.",
            "example": "1024"
          },
          "instagram_media_id": {
            "type": "string",
            "nullable": true,
            "description": "The Instagram media id the automation listens on. Set once the post is live on Instagram.",
            "example": "17912345678901234"
          },
          "post": {
            "type": "object",
            "nullable": true,
            "description": "A snapshot of the post: `source` (`omnisocials` or `instagram`), `id`, `caption`, `thumbnail_url`, `permalink`, `media_type` and, for an OmniSocials post, `status` and `live`."
          },
          "trigger": {
            "$ref": "#/components/schemas/AutomationTrigger"
          },
          "actions": {
            "$ref": "#/components/schemas/AutomationActions"
          },
          "limits": {
            "$ref": "#/components/schemas/AutomationLimits"
          },
          "stats": {
            "$ref": "#/components/schemas/AutomationStats"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T14:32:10.000Z"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-10-20T14:35:42.000Z"
          }
        }
      },
      "AutomationInput": {
        "type": "object",
        "description": "The fields of an automation. POST /automations requires `actions`; PATCH /automations/{id} takes any subset.",
        "properties": {
          "name": {
            "type": "string",
            "description": "A label for the automations list (max 120 characters).",
            "example": "Guide DM"
          },
          "trigger": {
            "$ref": "#/components/schemas/AutomationTrigger"
          },
          "post_id": {
            "type": "string",
            "description": "For `scope` `post`: an OmniSocials post that goes to Instagram (scheduled or live). One automation per post.",
            "example": "1024"
          },
          "instagram_media_id": {
            "type": "string",
            "description": "For `scope` `post`: the numeric id of a post or reel that was published outside OmniSocials (from the Instagram API, `GET /me/media`). The post is read from Instagram to check it belongs to the connected account. Pass `post_id` or `instagram_media_id`, not both.",
            "example": "17912345678901234"
          },
          "actions": {
            "$ref": "#/components/schemas/AutomationActions"
          },
          "limits": {
            "$ref": "#/components/schemas/AutomationLimits"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused"
            ],
            "description": "`paused` creates the automation without turning it on. Default `active`."
          }
        }
      },
      "AutomationEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "automation_id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "comment",
              "dm",
              "story_reply"
            ],
            "description": "What the person did."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "sent",
              "skipped",
              "failed",
              "pending"
            ]
          },
          "reason": {
            "type": "string",
            "nullable": true,
            "description": "For `skipped`: `already_sent`, `limit_reached`, `stopped`, `window_passed` (comment older than 7 days), `rate_limited`. For `failed`: the platform's error text."
          },
          "reason_label": {
            "type": "string",
            "nullable": true,
            "description": "The reason in plain words."
          },
          "person": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The Instagram-scoped id of the person."
              },
              "username": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "trigger_id": {
            "type": "string",
            "nullable": true,
            "description": "The comment id or message id that started the event."
          },
          "trigger_text": {
            "type": "string",
            "nullable": true,
            "description": "The comment or message text (300 characters). For `dm` and `story_reply` events it is returned only to a key with the `inbox:read` scope (OAuth: the social inbox permission); otherwise `null`."
          },
          "message": {
            "type": "string",
            "nullable": true,
            "description": "The DM that was sent."
          },
          "public_reply": {
            "type": "string",
            "nullable": true,
            "description": "The public reply posted under the comment, when one was."
          },
          "platform_message_id": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "sent_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      }
    }
  },
  "paths": {
    "/posts": {
      "get": {
        "summary": "List posts",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "in_approval",
                "scheduled",
                "posting",
                "published",
                "failed",
                "warning",
                "rejected"
              ]
            },
            "description": "Filter by status. `published` and the legacy alias `posted` are equivalent. `in_approval` matches posts waiting for a reviewer in an approval workflow. `rejected` matches posts an approver declined via POST /posts/{id}/reject. `warning` matches partially failed posts (some platforms published, some failed). These do NOT match `failed`, which only matches posts where every platform failed."
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            },
            "description": "Number of posts to return"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "Number of posts to skip"
          }
        ],
        "responses": {
          "200": {
            "description": "List of posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Post"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/posts?status=scheduled&limit=20\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst posts = await client.posts.list({ status: \"scheduled\", limit: 20 });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nposts = client.posts.list(status=\"scheduled\", limit=20)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:list --status scheduled --limit 20"
          }
        ]
      }
    },
    "/posts/recent-platform": {
      "get": {
        "summary": "List live platform posts",
        "description": "Returns the most recent posts pulled directly from each connected platform's API (Instagram, TikTok, X, YouTube, Facebook, LinkedIn, and more), including content that was published outside OmniSocials. This is the fallback for brand-new workspaces that have not published anything through OmniSocials yet, so GET /posts (which only lists OmniSocials posts) comes back empty. Each post is enriched with normalized `engagement` / `impressions` / `engagement_rate` so you can rank across platforms without re-deriving per-platform rules. Metrics are only present where the platform exposes them for historical posts: X, TikTok (views/likes/comments/shares, plus `average_time_watched`, `full_video_watched_rate`, `total_time_watched`, `favorites`, `reach` when the workspace enabled TikTok comments, the Business API authorization), Bluesky, Mastodon, Instagram (likes/comments/reach/views/saves/shares, via one insights call per post), Facebook, and YouTube return counts, while Threads, Pinterest, and Google Business return captions only (`metrics: {}`). LinkedIn personal profiles cannot be listed live (LinkedIn grants no such permission to apps), so their results contain posts published through OmniSocials with their latest collected stats; TikTok results are backfilled the same way for photo posts the video list omits. Fetched live on every call for most platforms, so expect a few seconds of latency across many platforms. X is the exception: because X's API bills every returned post, X results may be served from a snapshot up to 24 hours old. The snapshot refreshes immediately after you publish to X through OmniSocials, so your own new posts always appear. Requires the `analytics:read` scope.\n",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 50
            },
            "description": "Max posts to fetch per connected platform (1-50, default 25). When omitted, X returns up to 10 (its API bills per returned post); pass an explicit limit to get more from X.\n"
          },
          {
            "in": "query",
            "name": "platforms",
            "schema": {
              "type": "string",
              "example": "instagram,tiktok"
            },
            "description": "Optional comma-separated platform filter (e.g. \"instagram,tiktok\"). Defaults to every connected platform. Unsupported values return 400.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Recent posts across connected platforms, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "platform": {
                            "type": "string",
                            "example": "instagram"
                          },
                          "id": {
                            "type": "string",
                            "nullable": true,
                            "example": "17912345678901234",
                            "description": "The platform's own native post/media id (NOT the OmniSocials Post id). Use this as the stable key to store and de-duplicate each post when ingesting native content."
                          },
                          "permalink": {
                            "type": "string",
                            "nullable": true,
                            "description": "Public URL of the post on the platform. Present for Instagram, Facebook, Threads, TikTok, YouTube, X, Pinterest, Mastodon, Bluesky, and LinkedIn; null where the platform's listing API exposes no URL (e.g. Google Business) or one can't be built.",
                            "example": "https://www.instagram.com/p/Cxyz123ABC/"
                          },
                          "text": {
                            "type": "string",
                            "description": "Full, untruncated caption / post text (empty string if the post has none).",
                            "example": "Our autumn menu is here. #autumnmenu"
                          },
                          "format": {
                            "type": "string",
                            "description": "post, carousel, reel, video, image, text, or pin",
                            "example": "carousel"
                          },
                          "media_count": {
                            "type": "integer",
                            "example": 3
                          },
                          "timestamp": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-10-21T09:00:07.000Z"
                          },
                          "image_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Display image for the post where the platform exposes one (currently Instagram: photo, video poster, or first carousel frame). Null otherwise."
                          },
                          "duration_seconds": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Video length in whole seconds where the platform's listing API reports it (currently TikTok and YouTube). Null for images and for platforms that don't expose it (Instagram's media API has no duration field)."
                          },
                          "metrics": {
                            "type": "object",
                            "description": "Raw per-platform metrics as exact integers (never rounded/abbreviated); only keys the platform returned are present."
                          },
                          "engagement": {
                            "type": "number",
                            "example": 104
                          },
                          "impressions": {
                            "type": "number",
                            "example": 1520
                          },
                          "engagement_rate": {
                            "type": "number",
                            "description": "Percentage (0-100), suppressed below a minimum impression floor.",
                            "example": 6.84
                          }
                        }
                      }
                    },
                    "count": {
                      "type": "integer",
                      "example": 25
                    },
                    "connected_platforms": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "example": "instagram"
                      }
                    },
                    "errors": {
                      "type": "object",
                      "description": "Per-platform error messages for any platform that failed to fetch."
                    },
                    "note": {
                      "type": "string"
                    },
                    "current_date": {
                      "type": "string",
                      "format": "date",
                      "example": "2026-10-21"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "One or more requested platforms are not supported",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/posts/recent-platform?limit=10&platforms=instagram,x\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst recent = await client.posts.recentPlatform({ limit: 10, platforms: [\"instagram\", \"x\"] });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nrecent = client.posts.recent_platform(limit=10, platforms=[\"instagram\", \"x\"])"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:recent-platform --limit 10 --platforms instagram,x"
          }
        ]
      }
    },
    "/posts/{id}": {
      "get": {
        "summary": "Get a post",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            },
            "description": "Post ID"
          }
        ],
        "responses": {
          "200": {
            "description": "Post details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Post"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Post not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/posts/12345\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst post = await client.posts.get(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\npost = client.posts.get(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:get 12345"
          }
        ]
      },
      "patch": {
        "summary": "Update a post",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "object"
                  },
                  "accounts": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "instagram"
                    }
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "post",
                      "story",
                      "reel"
                    ],
                    "description": "Change post type (draft only)"
                  },
                  "media": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/media"
                  },
                  "media_ids": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/media"
                  },
                  "media_urls": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/media_urls"
                  },
                  "schedule_at": {
                    "type": "string",
                    "format": "date-time",
                    "example": "2026-10-21T09:00:00Z",
                    "description": "New publish time (ISO 8601, must be in the future). A draft becomes `scheduled` (or stays a planned draft when `draft: true` is sent with it). A post that is waiting for approval keeps its `in_approval` status and only its time moves. Send `null` to unschedule back to a `draft` without a date; a post that was waiting for approval then leaves its approval workflow (`approval_status` becomes `none`)."
                  },
                  "draft": {
                    "type": "boolean",
                    "example": false,
                    "description": "Planned-draft switch. `false` without `schedule_at` schedules a draft at the `schedule_at` it already carries (400 `validation_error` when it has none or it is in the past). `true` without `schedule_at` unschedules a scheduled post: it keeps its time and stays on the calendar as a dashed draft, nothing publishes; a post waiting for approval leaves its workflow. `true` with `schedule_at` moves the time and keeps (or makes) the post a draft."
                  },
                  "media_fit": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/media_fit"
                  },
                  "location_id": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/location_id"
                  },
                  "collaborators": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/collaborators"
                  },
                  "user_tags": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/user_tags"
                  },
                  "video_cover": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/VideoCover"
                      }
                    ],
                    "nullable": true,
                    "description": "Replaced wholesale; send the full desired cover. Pass `null` to remove the cover. Omit to leave it untouched. Facebook's `facebook.thumbnail_type` / `thumb_offset` / `cover_url` merge into `overrides.facebook` on their own (`thumbnail_type: null` clears just that override)."
                  },
                  "pinterest": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/pinterest"
                  },
                  "youtube": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/youtube"
                  },
                  "instagram": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/instagram"
                      }
                    ],
                    "description": "Instagram Reel options. MERGED into the stored options: keys you omit keep their current value (so updating share_to_feed cannot silently clear a Trial Reel flag or reel music set elsewhere); pass a key explicitly as null to clear it."
                  },
                  "tiktok": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/tiktok"
                  },
                  "x": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/x"
                  },
                  "bluesky": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/bluesky"
                  },
                  "mastodon": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/mastodon"
                  },
                  "threads": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/threads"
                  },
                  "telegram": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/telegram"
                      }
                    ],
                    "description": "Telegram options. MERGED into the stored options: keys you omit keep their value; a key set to null removes that option; `telegram: null` removes every option (the publish result stays)."
                  },
                  "google_business": {
                    "$ref": "#/components/schemas/CreatePostRequest/properties/google_business"
                  },
                  "linkedin": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/linkedin"
                      }
                    ],
                    "description": "LinkedIn Profile options. MERGED into the stored options: keys you omit keep their current value. `first_comment`: a string sets it, `null`/empty clears it. `carousel_as_images`: `true` publishes 2+ images as a plain image gallery, `false`/`null` reverts to the default PDF document carousel. `document_source`: `slides` rebuilds the document from the slide images, `original_pdf`/`null` reverts to sending the kept original PDF (the default)."
                  },
                  "linkedin_page": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/linkedin_page"
                      }
                    ],
                    "description": "LinkedIn Company Page options. Same merge semantics as `linkedin`."
                  },
                  "linkedin_poll": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/linkedin_poll"
                      }
                    ],
                    "description": "Replaced wholesale: send the full desired state for BOTH channels, not just the one you're changing. Pass `null` (or `{ \"linkedin\": null, \"linkedin_page\": null }`) to clear every poll and revert to normal posts; set just one channel's key to `null` to clear only that channel's poll while keeping the other."
                  },
                  "comment_dm_automation": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CreatePostRequest/properties/comment_dm_automation"
                      }
                    ],
                    "nullable": true,
                    "description": "Replaces the post's comment-to-DM automation (keywords, DM, public reply, limits); the automation keeps listening on the same post. Pass `null` to remove it. Omit to leave it untouched. Needs the `automations:write` scope."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Post updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Post"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Common cases:\n- Cannot update posted/failed posts\n- Per-platform media cap exceeded after the update (Bluesky/X/Mastodon ≤4, Instagram/Threads ≤10, TikTok ≤35), or mixed image+video on a platform that doesn't support it (Facebook, LinkedIn, X, Bluesky, Mastodon, TikTok. Instagram/Threads mixed carousels are fine)\n- Pinterest carousel aspect ratios don't all match\n- Character-limit exceeded for one or more selected platforms\nSame `validation_error` code + message shape as `POST /v1/posts/create`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Post not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_unavailable`: only with `media_urls`. Our storage provider is temporarily unavailable, the media was not stored and no post was changed. The URLs themselves are fine. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/posts/12345\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"schedule_at\": \"2026-11-02T09:00:00Z\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst post = await client.posts.update(\"12345\", { scheduled_at: \"2026-11-02T09:00:00Z\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\npost = client.posts.update(\"12345\", scheduled_at=\"2026-11-02T09:00:00Z\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:update 12345 --schedule 2026-11-02T09:00:00Z"
          }
        ]
      },
      "delete": {
        "summary": "Delete a post",
        "description": "Removes the post from OmniSocials only: it leaves the calendar, the posts list and the analytics. It never deletes the live post on a platform, so a published post stays live everywhere it went. To take a live post down as well, use \"Delete from [platform]\" on the post in the OmniSocials dashboard (all platforms except Instagram and TikTok, whose APIs have no delete) or delete it in the platform's own app. Cannot be undone.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Post deleted"
          },
          "404": {
            "description": "Post not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/posts/12345\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.posts.delete(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.posts.delete(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:delete 12345"
          }
        ]
      }
    },
    "/posts/create": {
      "post": {
        "summary": "Create a post",
        "description": "Create a new post. You can attach media by uploading first (media/media_ids) or by passing external URLs (media_urls) which will be downloaded and processed automatically. Both can be combined.\n\n**Required media:** Instagram and TikTok posts need at least one image/video. Pinterest posts need an image and a board_id. Posts without required media will be rejected with a 400 error.\n\n**Stories (`type: \"story\"`):** 1 to 10 media items. Each item is one slide and publishes as its own story on Instagram and Facebook, in the order given. Per-platform objects can give each platform its own slide list. Videos max 60 s per slide. After publishing, `story_slides` lists every slide's native id and URL; `published_urls` keeps the first slide.\n\n**Per-platform media caps (enforced at submit, not at publish time):**\n- `bluesky`: max 4 images per post; cannot mix images and video\n- `mastodon`: max 4 media items per post; cannot mix images and video\n- `x`: max 4 images per post; cannot mix images and video\n- `instagram`: max 10 items in a carousel; images and videos **can** be mixed in one carousel\n- `threads`: max 10 items in a carousel; images and videos **can** be mixed in one carousel\n- `tiktok`: max 35 items per photo post; cannot mix photos and videos in one post\n- `pinterest`: max 5 **images** per carousel pin (videos are exempt: they publish as single video pins)\n- `facebook`, `linkedin`, `linkedin_page`: cannot mix images and video in one post (their APIs accept images only or a single video)\n\nExceeding any cap returns `400 { error: { code: \"validation_error\", message: \"<Platform> allows up to N media items per post. You attached M, please remove M-N.\" } }` (Pinterest wording: \"Pinterest carousel pins support a maximum of 5 images. Please remove some images and try again.\"). The Pinterest carousel aspect-ratio constraint (2–5 same-ratio images) is enforced the same way; see the Pinterest section for the `mismatched_slides` response shape.\n\n**Per-platform video file-size caps (validated via ffprobe; drafts exempt):**\nMastodon 99 MB · Bluesky 100 MB · Instagram 300 MB · X 512 MB free tier, 16 GB with Premium / Premium+ (in practice bounded by the 1 GB URL-ingest / 100 MB direct-upload limits) · Threads / Reddit 1 GB · Pinterest 2 GB · Facebook / TikTok 4 GB · LinkedIn 5 GB · YouTube 256 GB.\nUpload requests are capped at 100 MB on top of these: anything bigger is rejected before media processing with `code: file_too_large`.\n\n**Per-platform video duration caps (validated via ffprobe; drafts exempt):**\nFacebook 240 min (Post + Reel; the separate 90 s Reel cap was removed on 2026-09-30) · Instagram 15 min (Post + Reel) · TikTok 10 min (Post + Reel) · YouTube Short **3 min** · X 140 s free tier, 125 min when the connected X account has Premium or Premium+ (tier detected automatically; reconnect X after upgrading) · Bluesky 180 s · Threads 5 min · LinkedIn 10 min · Pinterest 15 min · Reddit 15 min · Mastodon (instance-dependent).\n\nCap violations return `400 { error: { code: \"validation_error\", message: \"<Platform> only allows videos up to <cap>; yours is <duration>. Trim the video or deselect <Platform>.\" } }` (duration) or `\"This video (<size>) is too large for <Platform>. ...\"` (size). When multiple platforms reject the same upload, each contributes one sentence to the message. See `API_DEVELOPER_GUIDE.md` → \"Per-Platform Media Limits\" and \"Per-Platform Video Duration Caps\" for the full table with source citations.\n\n**X link-post credits:** X's API bills posts whose text contains a URL at a premium ($0.20 vs $0.015), and OmniSocials passes that fee through as prepaid credits at X's rate card (20 credits per URL-containing tweet; threads are billed per part that contains a link). Only a link written with `http://` or `https://` counts; a bare domain (`brand.com`) or a `www.` link is free. Credits are purchased in euros: 1 credit = €0.01, top-ups of €5-500 in the dashboard. When a create/schedule targets X and the text contains a URL, the 201 response includes a `warnings` array entry (`code: \"x_url_post_credits\"`) with `credits_required`, the company's current `credits_balance`, and the enforcement state. At publish time the credits are debited; if the balance can't cover it, the **X target alone fails** with an `INSUFFICIENT_CREDITS` message in its `error_message` (other platforms in the post publish normally) and the post can be retried after topping up in Settings → Organisation → Billing → Credits. Posts without links, analytics, and everything else on X stay free.\n\n**Schedule-time gate:** every scheduled X link post reserves its cost until it publishes. Scheduling (or publishing) a new X link post that would push the total reserved past the balance is refused with **402 `x_credits_insufficient`** (`details` carries `credits_required`, `credits_balance`, `credits_reserved`). Drafts are never gated; posts publishing before the enforcement date are never gated.\n",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Post"
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-blocking notices about this post. `x_url_post_credits` (X link-post fee pass-through) and `image_fit` (a feed image outside what a channel shows uncropped: `action` is `cropped`, `kept` or `crop_failed`, with `platform`, `media_url`, `ratio`, `target` and `crop_url`; see `media_fit`).",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "example": "x_url_post_credits"
                          },
                          "message": {
                            "type": "string"
                          },
                          "credits_required": {
                            "type": "integer",
                            "example": 20
                          },
                          "credits_balance": {
                            "type": "integer",
                            "nullable": true,
                            "example": 480
                          },
                          "enforced": {
                            "type": "boolean",
                            "example": true
                          },
                          "enforce_from": {
                            "type": "string",
                            "example": "2026-08-14"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. Common cases (the `error.code` field identifies which):\n- `validation_error`: missing required fields, unknown account/platform name (incl. composite ids with an unknown or mismatched workspace prefix, so always source ids from `GET /v1/accounts`), character-limit exceeded, per-platform media cap exceeded, mixed image+video on a platform that doesn't support it (Facebook, LinkedIn, X, Bluesky, Mastodon, TikTok. Instagram and Threads mixed carousels are allowed), Pinterest carousel aspect-ratio mismatch (`mismatched_slides` listed)\n- `platform_not_connected`: a selected platform isn't connected to this workspace\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`x_credits_insufficient`: this X link post plus the credits already reserved by the company's other scheduled X link posts exceed the balance. `error.details` carries `credits_required`, `credits_balance` and `credits_reserved`. Top up at https://app.omnisocials.com/credits (or remove the link from the X version) and retry the request. Only returned for non-draft creates targeting X with a URL while enforcement applies.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_unavailable`: only with `media_urls`. Our storage provider is temporarily unavailable, the media was not stored and no post was created. The URLs themselves are fine. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/create\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"content\": {\"default\": \"New drop this Friday\"},\n    \"accounts\": [\"linkedin\", \"x\"],\n    \"schedule_at\": \"2026-11-02T09:00:00Z\"\n  }'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst post = await client.posts.create({\n  content: \"New drop this Friday\",\n  channels: [\"linkedin\", \"x\"],\n  scheduled_at: \"2026-11-02T09:00:00Z\",\n});"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\npost = client.posts.create(\n    content=\"New drop this Friday\",\n    channels=[\"linkedin\", \"x\"],\n    scheduled_at=\"2026-11-02T09:00:00Z\",\n)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:create --text \"New drop this Friday\" --channels linkedin,x --schedule 2026-11-02T09:00:00Z"
          }
        ]
      }
    },
    "/posts/create-and-publish": {
      "post": {
        "summary": "Create and publish post",
        "description": "Creates a new post and publishes it right away, with no scheduling needed. Equivalent to calling POST /posts/create with publish_now: true. The post will be queued for immediate publishing to all selected platforms.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePostRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Post created and queued for immediate publishing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Post"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_unavailable`: only with `media_urls`. Our storage provider is temporarily unavailable, the media was not stored and no post was created. The URLs themselves are fine. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/create-and-publish\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"content\": {\"default\": \"We are live\"},\n    \"accounts\": [\"linkedin\", \"x\"]\n  }'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst post = await client.posts.createAndPublish({\n  content: \"We are live\",\n  channels: [\"linkedin\", \"x\"],\n});"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\npost = client.posts.create_and_publish(\n    content=\"We are live\",\n    channels=[\"linkedin\", \"x\"],\n)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:create-and-publish --text \"We are live\" --channels linkedin,x"
          }
        ]
      }
    },
    "/posts/{id}/publish": {
      "post": {
        "summary": "Publish a post now",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Post queued for publishing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "1024"
                        },
                        "status": {
                          "type": "string",
                          "example": "posting"
                        },
                        "message": {
                          "type": "string",
                          "example": "Post queued for immediate publishing."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Post cannot be published. Only draft or scheduled posts are publishable: an already published post returns `already_published`, a failed post returns `post_failed`, and any other status (`warning`, `posting`) returns `invalid_status`. Use POST /posts/{id}/retry for failed or partially failed posts. Also returned for validation problems (e.g., missing media for Instagram/TikTok, missing board_id for Pinterest, or Pinterest carousel slides at mismatched aspect ratios, listed in `mismatched_slides` on the error object)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Post not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/12345/publish\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst post = await client.posts.publish(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\npost = client.posts.publish(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:publish 12345"
          }
        ]
      }
    },
    "/posts/{id}/retry": {
      "post": {
        "summary": "Retry failed post",
        "description": "Re-publishes ONLY the platforms that failed, on the same post. Platforms that already succeeded are never re-published. Works on posts with status `failed` (every platform failed) or `warning` (some platforms failed). The retry is processed asynchronously by the publishing queue, usually within a minute; poll `GET /posts/{id}` to see the outcome (`status` becomes `published` and `published_urls` gains the platform URL on success, or the platform's entry reappears in `errors` on another failure). Each platform can be retried at most 3 times; after that, create a new post. Platforms dismissed with 'ignore' in the dashboard are not retried.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Retry queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "1024"
                        },
                        "status": {
                          "type": "string",
                          "example": "posting"
                        },
                        "platforms": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "example": "instagram"
                          },
                          "description": "The failed platforms being retried"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Post is not retryable: `invalid_status` (post is not failed/warning), `nothing_to_retry` (no failed platforms recorded, e.g. legacy posts without per-platform tracking), or `max_retries_reached` (a failed platform has already been retried 3 times; the response lists the exhausted platforms)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Post not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/12345/retry\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst retry = await client.posts.retry(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nretry = client.posts.retry(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:retry 12345"
          }
        ]
      }
    },
    "/posts/{id}/approve": {
      "post": {
        "summary": "Approve a post",
        "description": "Approves the current step of the post's approval workflow, on behalf of the user who owns this API key. That user must be a listed approver for the workflow's CURRENT step. Steps are approved in order, and a key holder who is only an approver on a later step gets `forbidden` until earlier steps clear. If this is the last step, the post is finalized immediately: it moves to `scheduled` (future `schedule_at`) or `posting` (due now or in the past). If more steps remain, the post stays `in_approval` and the next step's approvers are notified. Check `approval_status` on GET /posts/{id} first: only a post with status `in_approval` can be approved.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Approval recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "1024"
                        },
                        "status": {
                          "type": "string",
                          "description": "The post's status after this call: `in_approval` (more steps remain), `scheduled`, or `posting`.",
                          "example": "scheduled"
                        },
                        "approval_status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "approved"
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_status`: the post is not waiting for approval. `already_finalized`: someone else already decided this step (race condition).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`: the API key's owner is not a listed approver for the workflow's current step.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Post not found, or the post has no approval workflow attached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/12345/approve\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst result = await client.posts.approve(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nresult = client.posts.approve(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:approve 12345"
          }
        ]
      }
    },
    "/posts/{id}/reject": {
      "post": {
        "summary": "Reject a post",
        "description": "Rejects the post's approval workflow on behalf of the user who owns this API key. That user must be a listed approver for the workflow's CURRENT step. Unlike approval, a rejection stops the whole workflow immediately (not just the current step), and the post's status becomes `rejected`. An optional `comment` explaining the rejection is stored on the decision and posted into the post's review thread.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment": {
                    "type": "string",
                    "maxLength": 2000,
                    "description": "Optional reason for the rejection, shown to the requester and other approvers.",
                    "example": "Wrong product photo"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Rejection recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "1024"
                        },
                        "status": {
                          "type": "string",
                          "example": "rejected"
                        },
                        "approval_status": {
                          "type": "string",
                          "example": "rejected"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_status`: the post is not waiting for approval. `already_finalized`: someone else already decided this step (race condition). `validation_error`: `comment` was not a string.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`forbidden`: the API key's owner is not a listed approver for the workflow's current step.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Post not found, or the post has no approval workflow attached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/posts/12345/reject\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"comment\": \"Wrong CTA link, please fix.\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst result = await client.posts.reject(\"12345\", \"Wrong CTA link, please fix.\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nresult = client.posts.reject(\"12345\", comment=\"Wrong CTA link, please fix.\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:reject 12345 --comment \"Wrong CTA link, please fix.\""
          }
        ]
      }
    },
    "/posts/{id}/approval": {
      "get": {
        "summary": "Get approval review",
        "description": "Returns the review of a post that went through an approval workflow: every step with its approvers and their decisions, the rejection with its reason, and the comment thread. Use it after `approval_status` is `rejected` to learn who rejected the post and why, or while it is `pending` to see who the post waits for and what the reviewers wrote. A post without an approval workflow returns `status: none` with empty `steps` and `comments`.",
        "tags": [
          "Posts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            },
            "description": "Post ID"
          }
        ],
        "responses": {
          "200": {
            "description": "The approval review",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "post_id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "none",
                            "pending",
                            "approved",
                            "rejected"
                          ],
                          "description": "`none` = the post has no approval workflow."
                        },
                        "workflow": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "string",
                              "nullable": true,
                              "description": "Null for a one-off approval that was not made from a saved workflow"
                            },
                            "name": {
                              "type": "string"
                            }
                          }
                        },
                        "requested_by": {
                          "type": "object",
                          "nullable": true,
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "requested_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "current_step": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Order of the step the post waits on. Null when the review is finished."
                        },
                        "steps": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "order": {
                                "type": "integer"
                              },
                              "name": {
                                "type": "string"
                              },
                              "require_mode": {
                                "type": "string",
                                "enum": [
                                  "all",
                                  "any"
                                ],
                                "description": "`all` = every approver of the step must approve. `any` = one approval is enough."
                              },
                              "status": {
                                "type": "string",
                                "enum": [
                                  "pending",
                                  "approved",
                                  "rejected"
                                ]
                              },
                              "approvers": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "string"
                                    },
                                    "name": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "email": {
                                      "type": "string",
                                      "nullable": true
                                    },
                                    "status": {
                                      "type": "string",
                                      "enum": [
                                        "pending",
                                        "approved",
                                        "rejected"
                                      ]
                                    },
                                    "decided_at": {
                                      "type": "string",
                                      "format": "date-time",
                                      "nullable": true
                                    },
                                    "comment": {
                                      "type": "string",
                                      "nullable": true,
                                      "description": "The reason this approver gave with a rejection"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "rejection": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present when an approver rejected the post",
                          "properties": {
                            "by": {
                              "type": "object",
                              "properties": {
                                "id": {
                                  "type": "string"
                                },
                                "name": {
                                  "type": "string",
                                  "nullable": true
                                }
                              }
                            },
                            "reason": {
                              "type": "string",
                              "nullable": true
                            },
                            "at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "step": {
                              "type": "integer",
                              "nullable": true
                            }
                          }
                        },
                        "comments": {
                          "type": "array",
                          "description": "The review thread, oldest first. Includes the entries OmniSocials writes when a reviewer edits the post.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "author": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                  "id": {
                                    "type": "string"
                                  },
                                  "name": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              },
                              "message": {
                                "type": "string"
                              },
                              "account": {
                                "type": "string",
                                "nullable": true,
                                "description": "The channel the comment is about (`instagram`, `linkedin_page`, ...). Null = the whole post."
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "post_id": "123456",
                    "status": "rejected",
                    "workflow": {
                      "id": "42",
                      "name": "Content approval"
                    },
                    "requested_by": {
                      "id": "7d1f3c52-9a4e-4b6f-8c21-5e0a9d3b7f14",
                      "name": "Alex"
                    },
                    "requested_at": "2026-10-01T09:00:00.000Z",
                    "current_step": null,
                    "steps": [
                      {
                        "order": 1,
                        "name": "Team review",
                        "require_mode": "any",
                        "status": "approved",
                        "approvers": [
                          {
                            "id": "2b8e6f90-1c3d-4a57-9e84-6f2d0b1c3a75",
                            "name": "Sam",
                            "email": "sam@example.com",
                            "status": "approved",
                            "decided_at": "2026-10-01T10:15:00.000Z",
                            "comment": null
                          }
                        ]
                      },
                      {
                        "order": 2,
                        "name": "Client approval",
                        "require_mode": "all",
                        "status": "rejected",
                        "approvers": [
                          {
                            "id": "c4a09e1d-7b52-4f38-a6d1-3e9f8b2c5d60",
                            "name": "Jordan",
                            "email": "jordan@example.com",
                            "status": "rejected",
                            "decided_at": "2026-10-01T14:30:00.000Z",
                            "comment": "The image does not match the caption"
                          }
                        ]
                      }
                    ],
                    "rejection": {
                      "by": {
                        "id": "c4a09e1d-7b52-4f38-a6d1-3e9f8b2c5d60",
                        "name": "Jordan"
                      },
                      "reason": "The image does not match the caption",
                      "at": "2026-10-01T14:30:00.000Z",
                      "step": 2
                    },
                    "comments": [
                      {
                        "id": "5f3a2b1c-8d7e-4c69-b0a4-1e2d3c4b5a69",
                        "author": {
                          "id": "c4a09e1d-7b52-4f38-a6d1-3e9f8b2c5d60",
                          "name": "Jordan"
                        },
                        "message": "Please use the new logo",
                        "account": "instagram",
                        "created_at": "2026-10-01T14:28:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "404": {
            "description": "Post not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/posts/12345/approval\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst review = await client.posts.getApproval(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nreview = client.posts.get_approval(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials posts:approval 12345"
          }
        ]
      }
    },
    "/media": {
      "get": {
        "summary": "List media",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string",
              "example": "autumn"
            },
            "description": "Case-insensitive search across the media name and filename. Use this to find an existing asset by name instead of re-uploading it."
          },
          {
            "in": "query",
            "name": "folder_id",
            "schema": {
              "type": "string",
              "example": "12"
            },
            "description": "Return only media in this folder. Use \"root\" for unfiled items at the top level."
          }
        ],
        "responses": {
          "200": {
            "description": "List of media files",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Media"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/media?limit=20\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst media = await client.media.list({ limit: 20 });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nmedia = client.media.list(limit=20)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials media:list --limit 20"
          }
        ]
      }
    },
    "/media/{id}": {
      "get": {
        "summary": "Get a media item",
        "description": "Fetch one media item by id. Use this to poll a large URL upload until its `status` becomes `ready` (or `failed`).\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "482913"
            },
            "description": "Media id returned by an upload endpoint."
          }
        ],
        "responses": {
          "200": {
            "description": "The media item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Media"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Media item not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/media/67890\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst item = await client.media.get(\"67890\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nitem = client.media.get(\"67890\")"
          }
        ]
      },
      "delete": {
        "summary": "Delete a media item",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "482913"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Media deleted"
          },
          "404": {
            "description": "Media not found"
          },
          "409": {
            "description": "Media is attached to a scheduled or publishing post and cannot be deleted yet (error code `media_in_use`). Cancel or reschedule the post(s) first, otherwise deleting the file would make the post fail to publish."
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/media/67890\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.media.delete(\"67890\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.media.delete(\"67890\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials media:delete 67890"
          }
        ]
      },
      "patch": {
        "summary": "Rename or move media",
        "description": "Update the human-readable name and/or move a media file into a folder. Lets you label assets so they can be found later by name instead of re-uploading. Only the fields you send are changed.",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "482913"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Human-readable label (e.g. \"pp-play5get50\"). Send an empty string to clear it.",
                    "example": "autumn-menu-hero"
                  },
                  "folder_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Move the file into this folder. Send null to move it to the root (\"All media\").",
                    "example": "12"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Media updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Media"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No updatable fields provided, or folder not found"
          },
          "404": {
            "description": "Media not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/media/67890\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"hero-v2\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst item = await client.media.update(\"67890\", { name: \"hero-v2\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nitem = client.media.update(\"67890\", name=\"hero-v2\")"
          }
        ]
      }
    },
    "/media/upload": {
      "post": {
        "summary": "Upload a file",
        "description": "Upload an image or video, or a PDF. A PDF is rasterized into one image slide per page and returned as a carousel (see PdfUploadResult): pass all of `media_ids` to POST /posts. On LinkedIn the slides post as a native swipeable document; on Instagram, TikTok, Threads and Pinterest as an image carousel. PDFs are capped at 20 pages.\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to upload: an image, video, or PDF (max 100MB). Per-platform caps apply at scheduling: Mastodon 99MB, Bluesky 100MB, X 512MB (free tier), Threads/Reddit 1GB, Pinterest 2GB, Facebook/Instagram/TikTok 4GB, LinkedIn 5GB, YouTube 256GB. A PDF is split into image slides (max 20 pages) and returned as a carousel."
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional human-readable label (e.g. \"pp-play5get50\") so the asset is findable by name later. If the same bytes were already uploaded, the existing file is returned and labelled with this name.",
                    "example": "autumn-menu-hero"
                  },
                  "folder": {
                    "type": "string",
                    "description": "Optional folder name to file the asset under (created at the top level if it does not exist). Use `folder_id` instead to target an existing folder.",
                    "example": "Autumn campaign"
                  },
                  "folder_id": {
                    "type": "string",
                    "description": "Optional id of an existing folder to file the asset under.",
                    "example": "12"
                  },
                  "pdf_mode": {
                    "type": "string",
                    "enum": [
                      "slides",
                      "document"
                    ],
                    "default": "slides",
                    "description": "PDF uploads only. `slides` (default): one image media item per page, one id each; pass ALL of `media_ids` to POST /posts. `document`: ONE media item for the whole PDF (type `document`, its pages in `pdf.pages`); pass its single id in `media_ids` and the post gets every page, in order. Both modes keep the original file, which LinkedIn receives when the pages are posted unchanged."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File uploaded successfully. For an image or video the body has a single `data` (Media). For a PDF the body is a PdfUploadResult with `slides` + `media_ids` for the whole carousel.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Media"
                        },
                        "compatibility": {
                          "$ref": "#/components/schemas/MediaCompatibility"
                        },
                        "message": {
                          "type": "string",
                          "example": "File uploaded successfully. Use the 'id' in your post request."
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/PdfUploadResult"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid file or missing file",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Payload too large. This endpoint is hard-capped at 100MB and bodies above that are rejected by our CDN (Cloudflare) BEFORE reaching the API, so the response is a NON-JSON HTML page, not the usual JSON error envelope. To upload files up to 1GB, use POST /media/upload-from-url instead (the server fetches the URL and bypasses the body cap)."
          },
          "503": {
            "description": "`storage_unavailable`: our storage provider is temporarily unavailable and the file was not stored. The request itself is correct. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/upload\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -F \"file=@./photo.jpg\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst upload = await client.media.upload({ file: \"./photo.jpg\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nupload = client.media.upload(file=\"./photo.jpg\")"
          }
        ]
      }
    },
    "/media/upload-from-base64": {
      "post": {
        "summary": "Upload from base64",
        "description": "Upload a file from base64-encoded data. Useful for MCP integrations where the AI has image data but no URL. Accepts images, video, and PDF (a PDF is split into image slides and returned as a carousel, see PdfUploadResult; pass all of media_ids to POST /posts). Max 100MB after decoding; per-platform caps apply at scheduling (Mastodon 99MB, Bluesky 100MB, X 512MB free tier, Threads/Reddit 1GB, Pinterest 2GB, Facebook/Instagram/TikTok 4GB, LinkedIn 5GB, YouTube 256GB).",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "data",
                  "mime_type"
                ],
                "properties": {
                  "data": {
                    "type": "string",
                    "description": "Base64-encoded file data (without data URI prefix)"
                  },
                  "mime_type": {
                    "type": "string",
                    "description": "MIME type of the file (e.g. image/jpeg, image/png, application/pdf)",
                    "example": "image/jpeg"
                  },
                  "filename": {
                    "type": "string",
                    "description": "Optional filename",
                    "example": "autumn-menu.jpg"
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional human-readable label so the asset is findable by name later (e.g. \"pp-play5get50\").",
                    "example": "autumn-menu-hero"
                  },
                  "folder": {
                    "type": "string",
                    "description": "Optional folder name to file the asset under (created at the top level if missing).",
                    "example": "Autumn campaign"
                  },
                  "folder_id": {
                    "type": "string",
                    "description": "Optional id of an existing folder to file the asset under.",
                    "example": "12"
                  },
                  "pdf_mode": {
                    "type": "string",
                    "enum": [
                      "slides",
                      "document"
                    ],
                    "default": "slides",
                    "description": "PDF uploads only. `slides` (default): one image media item per page, one id each; pass ALL of `media_ids` to POST /posts. `document`: ONE media item for the whole PDF (type `document`, its pages in `pdf.pages`); pass its single id in `media_ids` and the post gets every page, in order. Both modes keep the original file, which LinkedIn receives when the pages are posted unchanged."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File uploaded successfully. A single `data` (Media) for image/video; a PdfUploadResult (`slides` + `media_ids`) for a PDF.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Media"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/PdfUploadResult"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid data or unsupported file type"
          },
          "413": {
            "description": "Payload too large. Decoded files are capped at 100MB, but because base64 inflates the request body ~33%, anything above ~75MB is rejected by our CDN (Cloudflare) BEFORE reaching the API and returns a NON-JSON HTML page. For large videos use POST /media/upload-from-url (up to 1GB) instead of base64."
          },
          "503": {
            "description": "`storage_unavailable`: our storage provider is temporarily unavailable and the file was not stored. The request itself is correct. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "DATA=$(base64 < chart.png | tr -d '\\n')\n\ncurl -X POST \"https://api.omnisocials.com/v1/media/upload-from-base64\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d \"{\\\"data\\\": \\\"$DATA\\\", \\\"mime_type\\\": \\\"image/png\\\", \\\"filename\\\": \\\"chart.png\\\"}\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { readFileSync } from \"node:fs\";\nimport { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst upload = await client.media.uploadFromBase64({\n  data: readFileSync(\"./chart.png\").toString(\"base64\"),\n  mime_type: \"image/png\",\n  filename: \"chart.png\",\n});"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "import base64\nfrom omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nwith open(\"chart.png\", \"rb\") as f:\n    data = base64.b64encode(f.read()).decode()\n\nupload = client.media.upload_from_base64(\n    data=data,\n    mime_type=\"image/png\",\n    filename=\"chart.png\",\n)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials media:upload-base64 --file ./chart.png"
          }
        ]
      }
    },
    "/media/upload-from-url": {
      "post": {
        "summary": "Upload from a URL",
        "description": "Download a file from a URL and upload it to your media library. It is the recommended path for MCP/programmatic uploads. Supports videos up to **1 GB**: files over 100 MB are streamed to storage by a background worker and the response returns `202` with the item in `processing` state (poll `GET /media/{id}` until `status` is `ready`). Files at or under 100 MB upload inline and return `201` ready. A PDF URL is split into image slides and returned as a carousel (see PdfUploadResult; pass all of `media_ids` to POST /posts). Every response also includes a `compatibility` block listing any connected platforms that would reject the file by size or format.\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTP or HTTPS URL of the file to download (max 1 GB for video; per-platform caps apply at scheduling).",
                    "example": "https://example.com/video.mp4"
                  },
                  "filename": {
                    "type": "string",
                    "description": "Optional filename override",
                    "example": "autumn-reel.mp4"
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional human-readable label so the asset is findable by name later (e.g. \"pp-play5get50\").",
                    "example": "autumn-menu-hero"
                  },
                  "folder": {
                    "type": "string",
                    "description": "Optional folder name to file the asset under (created at the top level if missing).",
                    "example": "Autumn campaign"
                  },
                  "folder_id": {
                    "type": "string",
                    "description": "Optional id of an existing folder to file the asset under.",
                    "example": "12"
                  },
                  "pdf_mode": {
                    "type": "string",
                    "enum": [
                      "slides",
                      "document"
                    ],
                    "default": "slides",
                    "description": "PDF uploads only. `slides` (default): one image media item per page, one id each; pass ALL of `media_ids` to POST /posts. `document`: ONE media item for the whole PDF (type `document`, its pages in `pdf.pages`); pass its single id in `media_ids` and the post gets every page, in order. Both modes keep the original file, which LinkedIn receives when the pages are posted unchanged."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "File uploaded inline (<= 100 MB) and ready to use. A single `data` (Media) for image/video; a PdfUploadResult (`slides` + `media_ids`) for a PDF.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "$ref": "#/components/schemas/Media"
                        },
                        "compatibility": {
                          "$ref": "#/components/schemas/MediaCompatibility"
                        },
                        "message": {
                          "type": "string",
                          "example": "File uploaded successfully. Use the 'id' in your post request."
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/PdfUploadResult"
                    }
                  ]
                }
              }
            }
          },
          "202": {
            "description": "Large file (over 100 MB) accepted and processing. `data.status` is 'processing': poll GET /media/{id} until 'ready' before using it.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Media"
                    },
                    "compatibility": {
                      "$ref": "#/components/schemas/MediaCompatibility"
                    },
                    "message": {
                      "type": "string",
                      "example": "Large file accepted and is being processed. Poll get media / list_media until its status is 'ready' before using it in a post."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid URL or unsupported file type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "File exceeds the 1 GB limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_unavailable`: our storage provider is temporarily unavailable and the file was not stored. The request itself is correct. Wait for the `Retry-After` header (seconds, also in the body as `retry_after`) and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/upload-from-url\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com/launch-video.mp4\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst upload = await client.media.uploadFromUrl({ url: \"https://example.com/launch-video.mp4\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nupload = client.media.upload_from_url(url=\"https://example.com/launch-video.mp4\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials media:upload --url https://example.com/launch-video.mp4"
          }
        ]
      }
    },
    "/media/create-upload-url": {
      "post": {
        "summary": "Create an upload URL",
        "description": "Create a one-time upload URL, for code that cannot send the API key with the upload, such as an AI assistant's code sandbox. POST the file as multipart `file` to `upload_url` (POST /media/upload-by-token) within 10 minutes; the URL works once. The MCP server's upload tool uses this flow. Requires the `media:write` scope.\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The one-time upload URL and how to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "upload_url": {
                      "type": "string",
                      "description": "POST the file here (POST /media/upload-by-token with the token in the query).",
                      "example": "https://api.omnisocials.com/v1/media/upload-by-token?upload_token=9f2c4e1ab07d"
                    },
                    "upload_token": {
                      "type": "string",
                      "description": "The one-time token (64 hex characters), also part of `upload_url`.",
                      "example": "9f2c4e1ab07d"
                    },
                    "expires_in_seconds": {
                      "type": "integer",
                      "example": 600
                    },
                    "method": {
                      "type": "string",
                      "example": "POST"
                    },
                    "content_type": {
                      "type": "string",
                      "example": "multipart/form-data"
                    },
                    "instructions": {
                      "type": "string",
                      "description": "Upload commands (curl and Python) written for an AI assistant's code sandbox."
                    },
                    "hint": {
                      "type": "string",
                      "description": "What an AI assistant should tell the user when its sandbox blocks the upload host."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks the `media:write` scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/create-upload-url\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst { upload_url } = await client.media.createUploadUrl();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nupload = client.media.create_upload_url()\nupload_url = upload[\"upload_url\"]"
          }
        ]
      }
    },
    "/media/{id}/split": {
      "post": {
        "summary": "Split a PDF into pages",
        "description": "For a media item of type `document` (a PDF kept as one item): creates one image media item per rendered page, in page order, each a copy of the page image and each carrying `pdf` with its `page` number. The document item stays. Use it when a single page is needed as a normal image. A full, ordered set of the slides still publishes the original PDF on LinkedIn. Requires `media:write`.\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "482913"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "The new slide items, in page order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Media"
                      }
                    },
                    "media_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The item is not a PDF document"
          },
          "404": {
            "description": "Media not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/67890/split\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          }
        ]
      }
    },
    "/media/check": {
      "post": {
        "summary": "Check media limits",
        "description": "Preflight a file against the platforms connected to your workspace and find out which (if any) would reject it by size or format, before you upload or post. Provide ONE of: a public `url` (we read its size/type via a HEAD request), an existing `media_id`, or an explicit `size_bytes` + `mime`. Use this to warn the user (\"this 995 MB video won't post to Instagram\") and confirm before uploading.\n",
        "tags": [
          "Media"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Public URL of the file to check (HEAD-probed for size + type).",
                    "example": "https://example.com/video.mp4"
                  },
                  "media_id": {
                    "type": "string",
                    "description": "Id of an already-uploaded library item to check.",
                    "example": "482913"
                  },
                  "size_bytes": {
                    "type": "integer",
                    "description": "File size in bytes (use with `mime` when you already know them).",
                    "example": 471859200
                  },
                  "mime": {
                    "type": "string",
                    "description": "MIME type, e.g. \"video/mp4\" (use with `size_bytes`).",
                    "example": "video/mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Compatibility result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "file": {
                      "type": "object",
                      "properties": {
                        "size_bytes": {
                          "type": "integer",
                          "nullable": true,
                          "example": 471859200
                        },
                        "mime": {
                          "type": "string",
                          "nullable": true,
                          "example": "video/mp4"
                        },
                        "size_known": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    },
                    "connected_platforms": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "example": "instagram"
                      }
                    },
                    "compatible": {
                      "type": "boolean",
                      "example": false,
                      "description": "true when no connected platform would reject the file."
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "platform": {
                            "type": "string",
                            "example": "instagram"
                          },
                          "display_name": {
                            "type": "string",
                            "example": "Instagram"
                          },
                          "reasons": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "example": "max 300 MB"
                            }
                          }
                        }
                      }
                    },
                    "summary": {
                      "type": "string",
                      "nullable": true,
                      "description": "One-line human summary, or null when fully compatible.",
                      "example": "This file (450 MB) won't post to: Instagram (max 300 MB)."
                    },
                    "fit": {
                      "type": "array",
                      "description": "How each connected channel with a fit rule shows this IMAGE as a feed post (sizes known for library images). `fits: false` = the ratio is outside the window the channel shows uncropped; `default_action` is what a post create does (`crop` = Instagram crops it to `target` around the subject; `keep` = Pinterest sends it as it is and reports it) unless the post's `media_fit` says otherwise. Empty for videos and unknown sizes.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "platform": {
                            "type": "string",
                            "example": "instagram"
                          },
                          "display_name": {
                            "type": "string",
                            "example": "Instagram"
                          },
                          "fits": {
                            "type": "boolean",
                            "example": false
                          },
                          "ratio": {
                            "type": "string",
                            "example": "9:16"
                          },
                          "shown": {
                            "type": "string",
                            "enum": [
                              "whole",
                              "cut"
                            ]
                          },
                          "target": {
                            "type": "string",
                            "nullable": true,
                            "example": "4:5"
                          },
                          "default_action": {
                            "type": "string",
                            "nullable": true,
                            "enum": [
                              "crop",
                              "keep"
                            ]
                          },
                          "message": {
                            "type": "string",
                            "nullable": true
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid parameters"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/check\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"url\": \"https://example.com/launch-video.mp4\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst check = await client.media.check({ url: \"https://example.com/launch-video.mp4\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\ncheck = client.media.check(url=\"https://example.com/launch-video.mp4\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials media:check --url https://example.com/launch-video.mp4"
          }
        ]
      }
    },
    "/media/upload-by-token": {
      "post": {
        "summary": "Upload with a token",
        "description": "Upload a file to a one-time URL from POST /media/create-upload-url. No API key: the `upload_token` query parameter is the auth, valid once for 10 minutes. Takes the same files as POST /media/upload (images, video, PDF; up to 100 MB). If storage is briefly down (503), the token stays valid and the same request can be sent again.\n",
        "tags": [
          "Media"
        ],
        "security": [],
        "parameters": [
          {
            "in": "query",
            "name": "upload_token",
            "required": true,
            "description": "The token from POST /media/create-upload-url.",
            "schema": {
              "type": "string",
              "example": "9f2c4e1ab07d"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "An image, video or PDF, up to 100 MB."
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional label so the file is findable by name later.",
                    "example": "autumn-menu-hero"
                  },
                  "pdf_mode": {
                    "type": "string",
                    "enum": [
                      "slides",
                      "document"
                    ],
                    "default": "slides",
                    "description": "PDF uploads only, as on POST /media/upload."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The stored file. For an image or video, `data` is the media item. For a PDF the body is a PdfUploadResult.\n",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "123"
                        },
                        "data": {
                          "$ref": "#/components/schemas/Media"
                        },
                        "status": {
                          "type": "string",
                          "example": "ready"
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/PdfUploadResult"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "No file, an unsupported or damaged file, or a bad `pdf_mode` (codes `missing_file`, `invalid_file_type`, `corrupt_media`, `upload_error`, `validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The token is unknown, already used (`invalid_token`) or expired (`token_expired`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`storage_unavailable`: nothing was stored and the token is still valid. Wait for `Retry-After` and send the same request again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/media/upload-by-token?upload_token=9f2c4e1ab07d\" \\\n  -F \"file=@./photo.jpg\""
          }
        ]
      }
    },
    "/folders": {
      "get": {
        "summary": "List folders",
        "description": "Returns all folders in the workspace as a flat list (build the tree client-side via parent_id). Use folder ids with the media endpoints to organize and find assets.",
        "tags": [
          "Folders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of folders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Folder"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/folders\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst folders = await client.folders.list();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nfolders = client.folders.list()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials folders:list"
          }
        ]
      },
      "post": {
        "summary": "Create a folder",
        "tags": [
          "Folders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Parent folder id to nest under, or omit for a top-level folder"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Folder created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid name or parent"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/folders\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Campaigns\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst folder = await client.folders.create({ name: \"Campaigns\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nfolder = client.folders.create(name=\"Campaigns\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials folders:create --name Campaigns"
          }
        ]
      }
    },
    "/folders/{id}": {
      "patch": {
        "summary": "Rename or move a folder",
        "tags": [
          "Folders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "New parent folder id, or null to move to the top level"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Folder updated"
          },
          "400": {
            "description": "Invalid update (e.g. would create a cycle)"
          },
          "404": {
            "description": "Folder not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/folders/42\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Campaigns 2026\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst folder = await client.folders.update(\"42\", { name: \"Campaigns 2026\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nfolder = client.folders.update(\"42\", name=\"Campaigns 2026\")"
          }
        ]
      },
      "delete": {
        "summary": "Delete a folder",
        "description": "Deletes the folder. Its media is NOT deleted - files move to the root (\"All media\"), and any subfolders move up to this folder's parent.",
        "tags": [
          "Folders"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Folder deleted"
          },
          "404": {
            "description": "Folder not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/folders/42\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.folders.delete(\"42\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.folders.delete(\"42\")"
          }
        ]
      }
    },
    "/hashtag-sets": {
      "get": {
        "summary": "List hashtag sets",
        "description": "Returns all saved hashtag sets in the workspace. Apply one to a new post by passing its name (`hashtag_set`) or id (`hashtag_set_id`) to POST /posts/create.",
        "tags": [
          "Hashtag Sets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of hashtag sets",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/HashtagSet"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/hashtag-sets\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst sets = await client.hashtagSets.list();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nsets = client.hashtag_sets.list()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials hashtag-sets:list"
          }
        ]
      },
      "post": {
        "summary": "Create a hashtag set",
        "description": "Saves a reusable, named group of hashtags. Tags may be passed with or without the leading '#'; they are deduped case-insensitively and stored in order. Max 100 tags per set.",
        "tags": [
          "Hashtag Sets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "hashtags"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Set name, e.g. \"Fitness Brand\". Unique per workspace."
                  },
                  "hashtags": {
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "description": "Array of tags ([\"fitness\", \"#gym\"]) or a single string (\"#fitness"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Hashtag set created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/HashtagSet"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid name/hashtags or duplicate name"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/hashtag-sets\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"Launch\", \"hashtags\": [\"saas\", \"buildinpublic\"]}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst set = await client.hashtagSets.create({ name: \"Launch\", hashtags: [\"saas\", \"buildinpublic\"] });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nhashtag_set = client.hashtag_sets.create(name=\"Launch\", hashtags=[\"saas\", \"buildinpublic\"])"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials hashtag-sets:create --name Launch --tags \"#saas #buildinpublic\""
          }
        ]
      }
    },
    "/hashtag-sets/{id}": {
      "get": {
        "summary": "Get a hashtag set",
        "tags": [
          "Hashtag Sets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Hashtag set",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/HashtagSet"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Hashtag set not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/hashtag-sets/7\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst set = await client.hashtagSets.get(\"7\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nhashtag_set = client.hashtag_sets.get(\"7\")"
          }
        ]
      },
      "patch": {
        "summary": "Update a hashtag set",
        "description": "Rename the set and/or replace its hashtags. `hashtags` replaces the full list (fetch, modify, resend to add/remove tags).",
        "tags": [
          "Hashtag Sets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "hashtags": {
                    "oneOf": [
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      {
                        "type": "string"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hashtag set updated"
          },
          "400": {
            "description": "Invalid update"
          },
          "404": {
            "description": "Hashtag set not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/hashtag-sets/7\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"hashtags\": [\"saas\", \"founder\"]}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst set = await client.hashtagSets.update(\"7\", { hashtags: [\"saas\", \"founder\"] });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nhashtag_set = client.hashtag_sets.update(\"7\", hashtags=[\"saas\", \"founder\"])"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials hashtag-sets:update 7 --tags \"#saas #founder\""
          }
        ]
      },
      "delete": {
        "summary": "Delete a hashtag set",
        "description": "Deletes the set. Posts that already used it are unaffected. The tags were merged into their captions at create time.",
        "tags": [
          "Hashtag Sets"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Hashtag set deleted"
          },
          "404": {
            "description": "Hashtag set not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/hashtag-sets/7\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.hashtagSets.delete(\"7\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.hashtag_sets.delete(\"7\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials hashtag-sets:delete 7"
          }
        ]
      }
    },
    "/approval-workflows": {
      "get": {
        "summary": "List approval workflows",
        "description": "Saved approval workflows the current workspace can use: company-wide workflows plus the ones bound to this workspace. Workflows are created and edited in the dashboard (Approvals); the API only lists them. Pass a workflow's `id` as `approval_workflow_id` on POST /posts/create to route a new scheduled post through it. The post is then created as `in_approval` and publishes at its `schedule_at` once the last step approves (dashboard, or POST /posts/{id}/approve). Requires the `posts:read` scope.",
        "tags": [
          "Approvals"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Workflows available to this workspace (may be empty)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApprovalWorkflow"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key"
          },
          "403": {
            "description": "`insufficient_scope`: the key lacks posts:read"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/approval-workflows\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst workflows = await client.approvalWorkflows.list();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nworkflows = client.approval_workflows.list()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials approval-workflows:list"
          }
        ]
      }
    },
    "/accounts": {
      "get": {
        "summary": "List accounts",
        "description": "Lists the social accounts connected to the workspace of this API key, one entry per channel. Requires the `accounts:read` scope.",
        "tags": [
          "Accounts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "platform",
            "schema": {
              "type": "string",
              "enum": [
                "instagram",
                "facebook",
                "linkedin",
                "linkedin_page",
                "youtube",
                "tiktok",
                "pinterest",
                "bluesky",
                "threads",
                "mastodon",
                "x",
                "google_business",
                "reddit",
                "snapchat",
                "telegram"
              ]
            },
            "description": "Filter by platform"
          }
        ],
        "responses": {
          "200": {
            "description": "List of connected accounts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Account"
                      }
                    },
                    "workspace_id": {
                      "type": "integer",
                      "description": "Stable id of the active workspace this key belongs to.",
                      "example": 1234
                    },
                    "workspace_name": {
                      "type": "string",
                      "nullable": true,
                      "description": "Human-readable name of the active workspace.",
                      "example": "Daily Edge Sports"
                    },
                    "workspace_icon": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/accounts\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst accounts = await client.accounts.list();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\naccounts = client.accounts.list()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials accounts:list"
          }
        ]
      }
    },
    "/accounts/{id}": {
      "get": {
        "summary": "Get an account",
        "tags": [
          "Accounts"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1234_instagram"
            },
            "description": "Account ID (format workspaceId_platform)"
          }
        ],
        "responses": {
          "200": {
            "description": "Account details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Account not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/accounts/844008_instagram\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst account = await client.accounts.get(\"844008_instagram\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\naccount = client.accounts.get(\"844008_instagram\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials accounts:get 844008_instagram"
          }
        ]
      }
    },
    "/analytics/posts": {
      "get": {
        "summary": "Get bulk post analytics",
        "description": "Batch version of GET /analytics/posts/{id}. Pass a comma-separated list of numeric OmniSocials post ids in `ids` and get each post's latest per-platform metrics back in a single request (thread posts are summed across their parts, same as the single-post endpoint). Built for sync clients that would otherwise call the single-post endpoint once per post and hit the 100 requests/minute rate limit. Up to 100 ids per call; page through larger sets in batches. Every requested id is returned (in request order); ids with no collected analytics come back with an empty `platforms` object rather than being omitted, so clients can map results 1:1 to the ids they sent. Reads stored snapshots from our database, the same data as the single-post endpoint, so it never calls the social platforms live.\n",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "ids",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024,1025,1026"
            },
            "description": "Comma-separated numeric OmniSocials post ids (the `id` field from GET /posts), e.g. \"1024,1025,1026\". Max 100 per request.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Per-post analytics, one entry per requested id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PostAnalytics"
                      }
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of entries returned (equals the number of valid ids requested)",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `ids`, more than 100 ids, or one or more ids that are not numeric OmniSocials post ids (e.g. a platform post id was supplied).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/analytics/posts?ids=12345,12346\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst stats = await client.analytics.posts([\"12345\", \"12346\"]);"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nstats = client.analytics.posts([\"12345\", \"12346\"])"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials analytics:posts 12345,12346"
          }
        ]
      }
    },
    "/analytics/posts/{id}": {
      "get": {
        "summary": "Get post analytics",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1024"
            },
            "description": "The numeric OmniSocials post id (the `id` field from GET /posts). Not a platform post id such as a YouTube video id or tweet id."
          }
        ],
        "responses": {
          "200": {
            "description": "Post analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PostAnalytics"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid post id (non-numeric, e.g. a platform post id was supplied instead of the OmniSocials post id)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/analytics/posts/12345\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst stats = await client.analytics.post(\"12345\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nstats = client.analytics.post(\"12345\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials analytics:post 12345"
          }
        ]
      }
    },
    "/analytics/overview": {
      "get": {
        "summary": "Get analytics overview",
        "description": "Totals for the workspace of this API key over a period (`period`, default 30 days, or `start_date` and `end_date`), with a breakdown per platform. Requires the `analytics:read` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "period",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "30d",
                "90d"
              ],
              "default": "30d"
            },
            "description": "Time period for analytics"
          },
          {
            "in": "query",
            "name": "start_date",
            "schema": {
              "type": "string",
              "example": "2026-09-01"
            },
            "description": "Custom start date. Accepts YYYY-MM-DD (e.g. \"2026-04-01\"), YYYY-MM month-shorthand (e.g. \"2026-04\" expands to the first of the month), or a full ISO 8601 datetime."
          },
          {
            "in": "query",
            "name": "end_date",
            "schema": {
              "type": "string",
              "example": "2026-09-30"
            },
            "description": "Custom end date. Same formats as start_date; \"YYYY-MM\" expands to the last day of the month."
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics overview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AnalyticsOverview"
                    },
                    "period": {
                      "type": "string",
                      "description": "Human-readable range label (e.g. \"30d\" or \"2026-04-01 to 2026-04-30\")",
                      "example": "30d"
                    },
                    "start_date": {
                      "type": "string",
                      "format": "date",
                      "description": "Resolved range start (YYYY-MM-DD), useful for clients that want to confirm what was queried",
                      "example": "2026-09-21"
                    },
                    "end_date": {
                      "type": "string",
                      "format": "date",
                      "description": "Resolved range end (YYYY-MM-DD)",
                      "example": "2026-10-21"
                    },
                    "current_date": {
                      "type": "string",
                      "format": "date",
                      "description": "Server's current date (UTC), included so clients can self-correct relative phrases like \"last month\"",
                      "example": "2026-10-21"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid date input (e.g. unparseable, or start_date after end_date)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/analytics/overview?period=30d\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst overview = await client.analytics.overview({ period: \"30d\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\noverview = client.analytics.overview(period=\"30d\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials analytics:overview --period 30d"
          }
        ]
      }
    },
    "/analytics/accounts": {
      "get": {
        "summary": "Get account analytics",
        "description": "Latest stored account-level snapshot per connected platform, on or before the requested date. Snapshots are collected daily. Metric semantics vary by platform (see the AccountAnalytics schema). LinkedIn rows hold that day's impressions (`period: \"daily\"`). A LinkedIn profile row also keeps the all-time total of all of the member's posts in `impressions_lifetime`; when LinkedIn serves no daily breakdown, its `impressions` is that lifetime total and the row carries `period: \"lifetime\"`. Each metrics object carries a `note` explaining its scope where semantics are non-obvious.\n",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "platform",
            "schema": {
              "type": "string",
              "example": "instagram"
            },
            "description": "Filter by platform"
          },
          {
            "in": "query",
            "name": "date",
            "schema": {
              "type": "string",
              "format": "date",
              "example": "2026-10-20"
            },
            "description": "Date to get analytics for (YYYY-MM-DD, defaults to today)"
          }
        ],
        "responses": {
          "200": {
            "description": "Account analytics data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccountAnalytics"
                      }
                    },
                    "date": {
                      "type": "string",
                      "format": "date",
                      "example": "2026-10-20"
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/analytics/accounts?platform=instagram\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst stats = await client.analytics.accounts({ platform: \"instagram\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nstats = client.analytics.accounts(platform=\"instagram\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials analytics:accounts --platform instagram"
          }
        ]
      }
    },
    "/analytics/best-times": {
      "get": {
        "summary": "Get best posting times",
        "description": "Recommended day/hour posting slots for one platform, computed from the workspace's own posting history (publish time × engagement of every post published on that platform, recency-weighted, outlier-damped). Times are bucketed in the requesting user's timezone (overridable via `timezone`). When the workspace has fewer than 15 analyzed posts on the platform, a static industry-average table is returned instead: `basis` flips from `own_data` to `defaults` and `posts_needed` says how many more published posts unlock personalized recommendations. Grid scores are relative within the platform (100 = the best slot). Requires the `analytics:read` scope.\n",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "platform",
            "required": true,
            "schema": {
              "type": "string",
              "example": "instagram"
            },
            "description": "Platform identifier (e.g. instagram, tiktok, linkedin, linkedin_page, x)."
          },
          {
            "in": "query",
            "name": "timezone",
            "schema": {
              "type": "string",
              "example": "Europe/Amsterdam"
            },
            "description": "IANA timezone for the buckets (e.g. Europe/Amsterdam). Defaults to the account's timezone, then UTC."
          }
        ],
        "responses": {
          "200": {
            "description": "Best-time grid and top recommendations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "platform": {
                      "type": "string",
                      "example": "instagram"
                    },
                    "timezone": {
                      "type": "string",
                      "description": "IANA timezone the day/hour buckets are expressed in",
                      "example": "Europe/Amsterdam"
                    },
                    "basis": {
                      "type": "string",
                      "enum": [
                        "own_data",
                        "own_data_and_audience",
                        "audience",
                        "defaults"
                      ],
                      "description": "`own_data` = computed from this workspace's posts; `own_data_and_audience` = own posts blended with when the audience is online (Instagram, TikTok Business); `audience` = not enough posts yet, based on the audience-online profile only; `defaults` = industry averages (not enough history yet)"
                    },
                    "sample_size": {
                      "type": "integer",
                      "description": "Number of analyzed posts (published >72h ago, last 365 days)",
                      "example": 48
                    },
                    "posts_needed": {
                      "type": "integer",
                      "description": "How many more published posts unlock personalized recommendations (0 when basis is own_data)",
                      "example": 0
                    },
                    "metric": {
                      "type": "string",
                      "description": "Always `engagement` in v1 (shared cross-platform engagement formula)",
                      "example": "engagement"
                    },
                    "window_days": {
                      "type": "integer",
                      "example": 365
                    },
                    "audience_online": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "number"
                      },
                      "description": "Hour (0-23, in the response timezone) -> number of followers online, from the platform's audience-activity data. Present for Instagram and TikTok Business accounts with 100+ followers once the daily account snapshot has collected it."
                    },
                    "audience_online_date": {
                      "type": "string",
                      "format": "date",
                      "description": "Snapshot date of `audience_online`.",
                      "example": "2026-10-20"
                    },
                    "grid": {
                      "type": "array",
                      "description": "One entry per day/hour cell that has data (missing cells = no posts there). Scores are relative 0-100.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string",
                            "enum": [
                              "monday",
                              "tuesday",
                              "wednesday",
                              "thursday",
                              "friday",
                              "saturday",
                              "sunday"
                            ]
                          },
                          "hour": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 23,
                            "example": 15
                          },
                          "score": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 100,
                            "example": 100
                          },
                          "n": {
                            "type": "integer",
                            "description": "Sample count behind the cell (±1h smoothed). Absent on defaults.",
                            "example": 6
                          },
                          "typical_engagement": {
                            "type": "integer",
                            "description": "Back-transformed typical engagement for the slot. Absent on defaults.",
                            "example": 132
                          }
                        }
                      }
                    },
                    "recommendations": {
                      "type": "array",
                      "description": "Top 3 slots, diversity-enforced (never two on the same day within 3 hours)",
                      "items": {
                        "type": "object",
                        "properties": {
                          "day": {
                            "type": "string",
                            "example": "tuesday"
                          },
                          "hour": {
                            "type": "integer",
                            "example": 15
                          },
                          "time": {
                            "type": "string",
                            "example": "15:00"
                          },
                          "score": {
                            "type": "integer",
                            "example": 100
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/unknown platform or invalid timezone"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/analytics/best-times?platform=instagram&timezone=Europe%2FAmsterdam\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst best = await client.analytics.bestTimes({ platform: \"instagram\", timezone: \"Europe/Amsterdam\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nbest = client.analytics.best_times(platform=\"instagram\", timezone=\"Europe/Amsterdam\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials analytics:best-times --platform instagram --timezone Europe/Amsterdam"
          }
        ]
      }
    },
    "/webhooks": {
      "get": {
        "summary": "List webhooks",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/webhooks\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst webhooks = await client.webhooks.list();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nwebhooks = client.webhooks.list()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:list"
          }
        ]
      },
      "post": {
        "summary": "Create a webhook",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS URL to receive events. Must resolve to a public host; private, loopback, link-local and unresolvable hosts are rejected with `validation_error`.",
                    "example": "https://example.com/webhooks/omnisocials"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "post.scheduled",
                        "post.published",
                        "post.failed",
                        "post.approved",
                        "post.rejected"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created. The secret is only returned once at creation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Webhook"
                        },
                        {
                          "type": "object",
                          "properties": {
                            "secret": {
                              "type": "string",
                              "description": "Webhook signing secret (only shown once)",
                              "example": "159ba257854f3f71a9b363b04367862705c72e3ed4cd746ecc11274f5c88291e"
                            }
                          }
                        }
                      ]
                    },
                    "message": {
                      "type": "string",
                      "example": "Webhook created. Save the secret - it will only be shown once. Use it to verify webhook signatures."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/webhooks\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com/omnisocials/webhook\",\n    \"events\": [\"post.published\", \"post.failed\"]\n  }'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst webhook = await client.webhooks.create({\n  url: \"https://example.com/omnisocials/webhook\",\n  events: [\"post.published\", \"post.failed\"],\n});"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nwebhook = client.webhooks.create(\n    url=\"https://example.com/omnisocials/webhook\",\n    events=[\"post.published\", \"post.failed\"],\n)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:create --url https://example.com/omnisocials/webhook --events post.published,post.failed"
          }
        ]
      }
    },
    "/webhooks/{id}": {
      "get": {
        "summary": "Get a webhook",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "74327072-ec54-473c-837f-63b29187ac16"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Webhook not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/webhooks/4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst webhook = await client.webhooks.get(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nwebhook = client.webhooks.get(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:get 4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90"
          }
        ]
      },
      "patch": {
        "summary": "Update a webhook",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "74327072-ec54-473c-837f-63b29187ac16"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://example.com/webhooks/omnisocials"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "post.scheduled",
                        "post.published",
                        "post.failed",
                        "post.approved",
                        "post.rejected"
                      ]
                    }
                  },
                  "is_active": {
                    "type": "boolean",
                    "example": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Webhook not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/webhooks/4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"is_active\": false}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst webhook = await client.webhooks.update(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\", { is_active: false });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nwebhook = client.webhooks.update(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\", is_active=False)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:update 4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90 --active false"
          }
        ]
      },
      "delete": {
        "summary": "Delete a webhook",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "74327072-ec54-473c-837f-63b29187ac16"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook deleted"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/webhooks/4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.webhooks.delete(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.webhooks.delete(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:delete 4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90"
          }
        ]
      }
    },
    "/webhooks/{id}/rotate-secret": {
      "post": {
        "summary": "Rotate webhook secret",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "74327072-ec54-473c-837f-63b29187ac16"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New secret generated. Save it now. It is shown only once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "example": "74327072-ec54-473c-837f-63b29187ac16"
                        },
                        "secret": {
                          "type": "string",
                          "description": "New webhook signing secret",
                          "example": "159ba257854f3f71a9b363b04367862705c72e3ed4cd746ecc11274f5c88291e"
                        }
                      }
                    },
                    "message": {
                      "type": "string",
                      "example": "Secret rotated. Save the new secret - it will only be shown once."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Webhook not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/webhooks/4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90/rotate-secret\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst rotated = await client.webhooks.rotateSecret(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nrotated = client.webhooks.rotate_secret(\"4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials webhooks:rotate-secret 4f6c2b1e-8a3d-4c7e-9b2f-1d5e6a7c8b90"
          }
        ]
      }
    },
    "/locations/search": {
      "get": {
        "summary": "Search locations",
        "description": "Search physical places to get a valid `location_id` for tagging a post. The `platform` query parameter selects the source, and the two sources use DIFFERENT ids (a Facebook Place ID is not a Threads location id), so always search with the platform you will tag.\n\n**`platform=instagram` (default):** search by name (`q`). Returns only real venues with a precise address (the kind Instagram accepts); national brand pages, personal profiles, and Instagram location IDs are not returned because Instagram rejects them. Pass a returned `id` as the top-level `location_id` on `POST /posts/create`. Requires a Facebook account connected to the workspace (the lookup runs through Facebook's place data). Response shape: `{ data: [...], error, needsPermission }`.\n\n**`platform=threads`:** search by name (`q`) OR by coordinates (`latitude` + `longitude`). Pass a returned `id` as `threads.location_id` on `POST /posts/create` / `PATCH /posts/{id}`. Requires a Threads account connected with the `threads_location_tagging` permission (connections made before that permission was added must be reconnected; the response then carries `error.code = threads_reauth_required`). Threads allows 500 location searches per account per rolling 24 hours. Response shape: `{ locations: [...] }` or `{ error: { code, message } }`.\n",
        "tags": [
          "Locations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "platform",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "instagram",
                "threads"
              ],
              "default": "instagram"
            },
            "description": "Which platform the location will be tagged on. Determines the search source and the id type returned."
          },
          {
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Place name to search (min 2 chars), e.g. a dealership, café, or venue name. Use a specific name: generic brands return their individual store locations, not the brand. Required for `platform=instagram`; for `platform=threads` pass either `q` or `latitude` + `longitude`.",
            "example": "Griffith Observatory"
          },
          {
            "in": "query",
            "name": "latitude",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "description": "Threads only. Search places around this point; pass together with `longitude` instead of `q`.",
            "example": 34.1184
          },
          {
            "in": "query",
            "name": "longitude",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "description": "Threads only. Pass together with `latitude`.",
            "example": -118.3004
          }
        ],
        "responses": {
          "200": {
            "description": "Matching places. The shape depends on `platform`.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "title": "Instagram results",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Use this as `location_id`",
                                "example": "313813441"
                              },
                              "name": {
                                "type": "string",
                                "example": "Griffith Observatory"
                              },
                              "address": {
                                "type": "string",
                                "nullable": true,
                                "example": "2800 E Observatory Rd, Los Angeles, CA, 90027, United States"
                              },
                              "city": {
                                "type": "string",
                                "nullable": true
                              },
                              "country": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "error": {
                          "type": "string",
                          "nullable": true,
                          "description": "Set when search is unavailable (e.g. no Facebook account connected, or the app lacks place-search permission). data is then empty."
                        },
                        "needsPermission": {
                          "type": "boolean",
                          "description": "true when the Facebook app lacks 'Page Public Content Access'."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "title": "Threads results",
                      "properties": {
                        "locations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string",
                                "description": "Use this as `threads.location_id`",
                                "example": "17841400000000000"
                              },
                              "name": {
                                "type": "string",
                                "nullable": true,
                                "example": "Griffith Observatory"
                              },
                              "address": {
                                "type": "string",
                                "nullable": true,
                                "example": "2800 E Observatory Rd"
                              },
                              "city": {
                                "type": "string",
                                "nullable": true,
                                "example": "Los Angeles"
                              },
                              "country": {
                                "type": "string",
                                "nullable": true,
                                "example": "United States"
                              },
                              "latitude": {
                                "type": "number",
                                "nullable": true,
                                "example": 34.1184
                              },
                              "longitude": {
                                "type": "number",
                                "nullable": true,
                                "example": -118.3004
                              }
                            }
                          }
                        },
                        "error": {
                          "type": "object",
                          "nullable": true,
                          "description": "Present instead of `locations` when the search could not run.",
                          "properties": {
                            "code": {
                              "type": "string",
                              "enum": [
                                "not_available",
                                "threads_not_connected",
                                "threads_reauth_required",
                                "platform_error"
                              ],
                              "description": "`not_available` = Threads location tagging is not available yet (the Threads location permission is still under Meta review). `threads_not_connected` = no Threads account on the workspace. `threads_reauth_required` = the Threads connection lacks the `threads_location_tagging` permission; reconnect Threads. `platform_error` = Threads did not answer; retry."
                            },
                            "message": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request (Threads only): neither `q` nor `latitude`+`longitude` given, `q` under 2 characters, coordinates out of range, or Threads rejected the search text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/locations/search?q=Griffith%20Observatory\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst places = await client.locations.search(\"Griffith Observatory\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nplaces = client.locations.search(\"Griffith Observatory\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials locations:search \"Griffith Observatory\""
          }
        ]
      }
    },
    "/locations/validate": {
      "get": {
        "summary": "Validate a location",
        "description": "Check whether a Facebook Place ID is a valid Instagram location (a single venue with a precise address) before using it as `location_id`. Returns `{ valid: true, name, address }` for taggable places; `{ valid: false, reason }` for personal profiles, brand pages, or non-places. If the workspace has no Facebook account (or the app can't read the page), returns `{ valid: false, unverified: true }`, and the post's publish step is then the final check.",
        "tags": [
          "Locations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Numeric Facebook Place ID to validate.",
            "example": "313813441"
          }
        ],
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string",
                      "nullable": true
                    },
                    "name": {
                      "type": "string",
                      "nullable": true
                    },
                    "address": {
                      "type": "string",
                      "nullable": true
                    },
                    "unverified": {
                      "type": "boolean",
                      "description": "Couldn't be checked now; the publish step will validate."
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "Why it isn't valid / couldn't be verified."
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/locations/validate?id=313813441\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst check = await client.locations.validate(\"313813441\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\ncheck = client.locations.validate(\"313813441\")"
          }
        ]
      }
    },
    "/audio/search": {
      "get": {
        "summary": "Search Instagram audio",
        "description": "Search Meta's licensed audio catalog for tracks that can be attached to an Instagram Reel. Pass a returned `audio_id` as `instagram.audio_id` on `POST /posts/create` (type `reel`). Omit `q` to get currently trending audio. Only tracks Meta has authorized for third-party publishing are returned, so the selection can differ from the native app. Requires a Facebook account connected to the workspace whose Facebook Page is linked to the workspace's Instagram account (music publishing runs through Facebook's side of the Instagram API).",
        "tags": [
          "Audio"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Song title, artist, or keyword. Omit for trending audio.",
            "example": "birthday"
          },
          {
            "in": "query",
            "name": "type",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "music",
                "original_sound"
              ],
              "default": "music"
            },
            "description": "Catalog to search: licensed music (default) or original sounds created on Instagram."
          }
        ],
        "responses": {
          "200": {
            "description": "Matching audio tracks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "audio_id": {
                            "type": "string",
                            "description": "Use this as `instagram.audio_id`",
                            "example": "587784541076604"
                          },
                          "title": {
                            "type": "string",
                            "nullable": true,
                            "example": "Birthday Wish"
                          },
                          "artist": {
                            "type": "string",
                            "nullable": true,
                            "example": "Shuba"
                          },
                          "duration_ms": {
                            "type": "integer",
                            "nullable": true,
                            "example": 153760
                          },
                          "audio_type": {
                            "type": "string",
                            "enum": [
                              "music",
                              "original_sound"
                            ]
                          },
                          "cover_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Cover artwork (music) or creator avatar (original sound)."
                          },
                          "preview_url": {
                            "type": "string",
                            "nullable": true,
                            "description": "Temporary audio preview URL. It expires after about 1.5 days, so do not store it."
                          },
                          "ig_username": {
                            "type": "string",
                            "nullable": true,
                            "description": "Creator handle (original sounds only)."
                          }
                        }
                      }
                    },
                    "error": {
                      "type": "string",
                      "nullable": true,
                      "description": "Set when search is unavailable (e.g. no linked Facebook account). data is then empty."
                    },
                    "needsFacebook": {
                      "type": "boolean",
                      "description": "true when the workspace needs a Facebook connection (with a Page linked to this Instagram account) before music is available."
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/audio/search?q=summer\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst tracks = await client.audio.search(\"summer\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\ntracks = client.audio.search(\"summer\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials audio:search summer"
          }
        ]
      }
    },
    "/pinterest/products": {
      "get": {
        "summary": "List product Pins",
        "description": "List the product Pins of the connected Pinterest account. Pass a returned `pin_id` in `pinterest.product_tags` on `POST /posts/create` / `PATCH /posts/{id}` to tag the product on a Pin (max 24 products per Pin).\n\nPinterest only accepts a product tag when the product Pin is public, belongs to the same Pinterest account, and links to a website that account claimed on Pinterest. Products of other merchants cannot be tagged through the API.\n\n**Sources.** `source=catalog` reads the Pinterest catalog (with `price`, `currency`, `availability`, `item_id`) through a product group; the response carries `product_groups` so you can pick another group with `product_group_id`. It needs catalog access on the Pinterest connection: in the OmniSocials composer open the Pinterest options, select Add products, then Connect catalog (one time per workspace). Without catalog access the response is `error.code = pinterest_catalog_access_required`. `source=pins` reads the account's own Pins and returns the ones Pinterest marks as product Pins; it works on every connection. One `pins` call scans up to 250 Pins, so `products` can be empty while `bookmark` is set: call again with the bookmark. When `source` is left out, `catalog` is used when the connection has catalog access, else `pins`.\n\nProblems that are not a bad request (no Pinterest account, no catalog access, Pinterest did not answer) come back as HTTP 200 with an `error` object instead of `products`.\n",
        "tags": [
          "Pinterest"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "source",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "catalog",
                "pins"
              ]
            },
            "description": "Where to read product Pins from. Default: `catalog` when the connection has catalog access, else `pins`."
          },
          {
            "in": "query",
            "name": "product_group_id",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Catalog source only. A product group `id` from `product_groups`. Default: the group named \"All Products\", else the first group.",
            "example": "443727193917"
          },
          {
            "in": "query",
            "name": "bookmark",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Cursor from the previous response, to get the next page."
          },
          {
            "in": "query",
            "name": "page_size",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            },
            "description": "Catalog source only. Products per page."
          }
        ],
        "responses": {
          "200": {
            "description": "Product Pins, or an `error` object when the list could not be read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "products": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PinterestProduct"
                      }
                    },
                    "bookmark": {
                      "type": "string",
                      "nullable": true,
                      "description": "Pass as `bookmark` to get the next page. `null` on the last page."
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "catalog",
                        "pins"
                      ],
                      "description": "The source that was read."
                    },
                    "catalog_access": {
                      "type": "boolean",
                      "description": "`true` when the Pinterest connection can read the catalog."
                    },
                    "no_catalog": {
                      "type": "boolean",
                      "description": "`true` when the connection has catalog access but the Pinterest account has no catalog yet (Pinterest answers HTTP 409 to the product group request). `products` and `product_groups` are then empty. Products are added at https://www.pinterest.com/business/catalogs/."
                    },
                    "product_groups": {
                      "type": "array",
                      "description": "Catalog source only. The product groups of the account.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "443727193917"
                          },
                          "name": {
                            "type": "string",
                            "nullable": true,
                            "example": "All Products"
                          }
                        }
                      }
                    },
                    "product_group_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "Catalog source only. The group these products come from."
                    },
                    "error": {
                      "type": "object",
                      "nullable": true,
                      "description": "Present instead of `products` when the list could not be read.",
                      "properties": {
                        "code": {
                          "type": "string",
                          "enum": [
                            "pinterest_not_connected",
                            "pinterest_catalog_access_required",
                            "platform_error"
                          ],
                          "description": "`pinterest_not_connected` = no Pinterest account on the workspace. `pinterest_catalog_access_required` = `source=catalog` was asked but the connection has no catalog access; connect the catalog in the composer or use `source=pins`. `platform_error` = Pinterest did not answer or refused; the message carries Pinterest's text."
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "products": [
                    {
                      "pin_id": "813744226420795884",
                      "title": "Blue ribbed top",
                      "description": null,
                      "link": "https://shop.example.com/products/blue-ribbed-top",
                      "image_url": "https://i.pinimg.com/400x300/aa/bb/cc/example.jpg",
                      "price": 24.99,
                      "currency": "EUR",
                      "availability": "IN_STOCK",
                      "item_id": "TOP-BLUE-M"
                    }
                  ],
                  "bookmark": null,
                  "source": "catalog",
                  "catalog_access": true,
                  "product_groups": [
                    {
                      "id": "443727193917",
                      "name": "All Products"
                    }
                  ],
                  "product_group_id": "443727193917"
                }
              }
            }
          },
          "400": {
            "description": "Bad request: `source` is not `catalog` or `pins`, or `product_group_id` is not a product group of the connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/pinterest/products\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst products = await client.pinterest.listProducts();"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nproducts = client.pinterest.list_products()"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials pinterest:products"
          }
        ]
      }
    },
    "/pinterest/products/validate": {
      "get": {
        "summary": "Check a product Pin",
        "description": "Check whether a Pin can be used in `pinterest.product_tags` before you create the post. Pass a Pin ID or a Pin link (`https://www.pinterest.com/pin/<id>/`; `pin.it` short links do not work). Returns `{ valid: true, pin_id, title, link, image_url }` for a product Pin of the connected account, and `{ valid: false, reason }` for a Pin that is not a product Pin, belongs to another account or does not exist. When the check cannot run (no Pinterest account, Pinterest did not answer, or Pinterest did not say if the Pin is a product Pin) it returns `{ valid: false, unverified: true }`: the publish step is then the final check. The check does not look at the claimed-website rule; Pinterest applies that one at publish.",
        "tags": [
          "Pinterest"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Pin ID or Pin link of the product Pin.",
            "example": "813744226420795884"
          }
        ],
        "responses": {
          "200": {
            "description": "Check result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "pin_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "The Pin ID read from `id` (`null` when `id` is not a Pin ID or link)."
                    },
                    "title": {
                      "type": "string",
                      "nullable": true
                    },
                    "link": {
                      "type": "string",
                      "nullable": true
                    },
                    "image_url": {
                      "type": "string",
                      "nullable": true
                    },
                    "unverified": {
                      "type": "boolean",
                      "description": "The check could not run; the publish step will check."
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "Why the Pin is not valid or could not be checked."
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/pinterest/products/validate?id=813744226420795884\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst check = await client.pinterest.validateProduct(\"813744226420795884\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\ncheck = client.pinterest.validate_product(\"813744226420795884\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials pinterest:validate 813744226420795884"
          }
        ]
      }
    },
    "/inbox/conversations": {
      "get": {
        "summary": "List conversations",
        "description": "Returns one entry per conversation (the latest message in each), across Instagram and Facebook DMs, comments, and mentions, LinkedIn company-page comments and mentions (LinkedIn has no DMs via API), TikTok video comments (comments only; requires the TikTok comments authorization on the channel), YouTube video comments (comments only; each channel is checked once per day, YouTube has no comment webhooks), X DMs for workspaces that opted in to X DMs in the dashboard (X is DM-only; no history from before opt-in), and Threads replies on your posts plus mentions (requires a Threads connection with the reply-reading permission). Sorted by newest activity first. Cursor-paginated: follow `pagination.next_cursor` until it is null. Requires the `inbox:read` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            },
            "description": "Max conversations per page (1-100, default 25)."
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string",
              "example": "MjAyNi0xMC0yMVQwOTowMDowMC4wMDBafDg4MjMx"
            },
            "description": "Opaque pagination cursor from a previous response."
          },
          {
            "in": "query",
            "name": "platform",
            "schema": {
              "type": "string",
              "enum": [
                "instagram",
                "facebook",
                "linkedin",
                "tiktok",
                "youtube",
                "x",
                "threads"
              ]
            },
            "description": "Filter to a single platform."
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "dm",
                "comment",
                "mention"
              ]
            },
            "description": "Filter to a single conversation type."
          },
          {
            "in": "query",
            "name": "unread",
            "schema": {
              "type": "boolean",
              "example": true
            },
            "description": "When true, only conversations with unread incoming messages."
          },
          {
            "in": "query",
            "name": "unanswered",
            "schema": {
              "type": "boolean",
              "example": true
            },
            "description": "When true, only conversations that still need an answer: the customer's latest DM has no reply after it (Instagram/Facebook DMs past the 24-hour messaging window included; they cannot be answered through the API but the customer is still waiting), or a comment/mention has not been replied to and is not hidden. Replies typed in the native apps count as answers (they are mirrored into the inbox). Read state is ignored here; use GET /inbox/next for a work queue.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of conversations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboxConversation"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/InboxCursorPagination"
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/inbox/conversations?platform=instagram&unread=true&limit=25\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst conversations = await client.inbox.listConversations({ platform: \"instagram\", unread: true, limit: 25 });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nconversations = client.inbox.list_conversations(platform=\"instagram\", unread=True, limit=25)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:list --platform instagram --unread --limit 25"
          }
        ]
      }
    },
    "/inbox/conversations/{conversationId}/messages": {
      "get": {
        "summary": "List messages",
        "description": "Returns the full message history of a conversation, oldest first, cursor-paginated. Requires the `inbox:read` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "conversationId",
            "required": true,
            "schema": {
              "type": "string",
              "example": "instagram_comment_17912345678901234"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string",
              "example": "MjAyNi0xMC0yMVQwOTowMDowMC4wMDBafDg4MjMx"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of messages",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/InboxMessage"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/InboxCursorPagination"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found in this workspace"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/inbox/conversations/instagram_1234567890_17841400000000000/messages\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst messages = await client.inbox.getMessages(\"instagram_1234567890_17841400000000000\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nmessages = client.inbox.get_messages(\"instagram_1234567890_17841400000000000\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:messages instagram_1234567890_17841400000000000"
          }
        ]
      }
    },
    "/inbox/conversations/{conversationId}/read": {
      "post": {
        "summary": "Mark conversation read",
        "description": "Marks every unread incoming message in the conversation as read and returns how many were updated. Requires the `inbox:write` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "conversationId",
            "required": true,
            "schema": {
              "type": "string",
              "example": "instagram_comment_17912345678901234"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Read state updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation_id": {
                      "type": "string",
                      "example": "instagram_comment_17912345678901234"
                    },
                    "marked_read": {
                      "type": "integer",
                      "example": 3
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Conversation not found in this workspace"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/inbox/conversations/instagram_1234567890_17841400000000000/read\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst result = await client.inbox.markRead(\"instagram_1234567890_17841400000000000\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nresult = client.inbox.mark_read(\"instagram_1234567890_17841400000000000\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:read instagram_1234567890_17841400000000000"
          }
        ]
      }
    },
    "/inbox/conversations/{conversationId}/reply": {
      "post": {
        "summary": "Reply to a conversation",
        "description": "Sends a reply in an existing conversation (DM, comment, or mention) on Instagram, Facebook, LinkedIn, TikTok (comments only), YouTube (comments only), X (DM-only), or Threads (replies and mentions; needs a Threads connection with the reply permission, else 401 `reauth_required`). You can only reply to conversations that already exist in your inbox. The API cannot start new conversations (Instagram requires the user to message first, and cold outreach violates Meta policy). Direct-message replies must be within the platform's messaging window (24 hours since the user's last message); replies outside the window are rejected. TikTok replies are text-only, capped at 150 characters (400 `validation_error` beyond that), and can take a few minutes to appear on TikTok while they pass spam review. Each workspace can send up to 1,000 replies per rolling 24 hours (429 `reply_limit_reached` beyond that). Contact robert@omnisocials.com if you need a higher limit. X DM replies debit 2 credits from the company balance before the send (auto-refunded if the send fails) and can return 402 `insufficient_credits` or 402 `x_inbox_suspended`. Requires the `inbox:write` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "conversationId",
            "required": true,
            "schema": {
              "type": "string",
              "example": "instagram_comment_17912345678901234"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Either `text` or `attachment_url` must be set.",
                "properties": {
                  "text": {
                    "type": "string",
                    "example": "It comes in S, M and L. Which size do you need?",
                    "description": "Reply body (max 2000 characters; Instagram DMs max 1000; TikTok comments max 150). Optional when `attachment_url` is set: an attachment-only DM is allowed.",
                    "maxLength": 2000
                  },
                  "attachment_url": {
                    "type": "string",
                    "example": "https://example.com/size-chart.jpg",
                    "description": "Optional media URL to send (Facebook and Instagram DMs only)"
                  },
                  "attachment_type": {
                    "type": "string",
                    "enum": [
                      "image",
                      "video",
                      "audio",
                      "file"
                    ],
                    "description": "Required if attachment_url is set"
                  },
                  "message_id": {
                    "type": "string",
                    "example": "88231",
                    "description": "For comment and mention threads: the inbox id of the specific incoming comment being answered (`message.id` from GET /inbox/next, or a message id from GET /inbox/conversations/{id}/messages). Every comment on a post shares one conversation, so without it the reply is posted under the newest comment on the post, which may be a different person than the one you drafted for. Always pass it when replying to an item served by the queue. Ignored for DMs (a DM reply goes to the conversation). 404 `not_found` when it is not an incoming message of this conversation."
                  },
                  "include_next": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, the response also carries `next`: the next conversation that needs an answer (same shape as GET /inbox/next, with its default queue order and filters), and `remaining`. Saves the extra call when working through the inbox."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reply sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/InboxMessage"
                    },
                    "next": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/InboxNextUnanswered"
                        }
                      ],
                      "description": "Only when `include_next` was set. Null when nothing is waiting."
                    },
                    "remaining": {
                      "type": "integer",
                      "example": 4,
                      "description": "Only when `include_next` was set: unanswered items still waiting after `next` (capped at 500)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (including `text` over the platform cap: 2000 overall, 1000 on Instagram DMs, 150 on TikTok comments), `unsupported_platform`, or `unsupported_reply_type` (the conversation's platform/type cannot be replied to, e.g. an Instagram story mention)"
          },
          "402": {
            "description": "X DMs only: `insufficient_credits` (balance cannot cover the 2-credit send) or `x_inbox_suspended` (X inbox paused at zero balance; top up and re-enable)"
          },
          "403": {
            "description": "Platform limitation (e.g. cannot comment on a personal-profile post)"
          },
          "404": {
            "description": "`not_found`: conversation not found in this workspace, `message_id` is not an incoming message of this conversation, or the comment/message being replied to no longer exists on the platform (deleted, hidden, or restricted by its author)"
          },
          "409": {
            "description": "`duplicate_reply`: this exact reply (same text and attachment, same comment or DM conversation) already went out in the last 10 minutes, or is being sent right now; nothing was sent again. `data` carries the earlier outgoing message when there is one. Treat it as done: do not resend."
          },
          "422": {
            "description": "`outside_messaging_window`: Instagram/Facebook DM replies are only allowed within 24 hours of the customer's last message (Meta policy); checked before the send, the message names when the window closed (GET /inbox/next reports the same in `reply_window`). Answer such a DM from the native app (mirrored into the inbox) or mark the conversation read. `reply_not_allowed`: the platform refuses replies to this item for good (Facebook #1705: the comment sits on a share or boosted copy of the post, or the commenter restricts replies; YouTube: the thread's canReply is false because comments are off on the video or the comment was removed or held for review). The item is dropped from GET /inbox/next; do not retry."
          },
          "429": {
            "description": "Daily reply limit reached for this workspace"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/inbox/conversations/instagram_1234567890_17841400000000000/reply\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"text\": \"Yes, we ship worldwide!\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst reply = await client.inbox.reply(\"instagram_1234567890_17841400000000000\", { text: \"Yes, we ship worldwide!\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nreply = client.inbox.reply(\"instagram_1234567890_17841400000000000\", \"Yes, we ship worldwide!\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:reply instagram_1234567890_17841400000000000 --text \"Yes, we ship worldwide!\""
          }
        ]
      }
    },
    "/inbox/messages/{id}/hide": {
      "post": {
        "summary": "Hide or unhide a comment",
        "description": "Hides (or unhides) a comment someone left on one of your posts, on the platform, as the post owner. Facebook, Instagram, TikTok, YouTube and Threads comments (Threads: incoming top-level replies only; Threads does not allow hiding nested replies). On YouTube \"hide\" sets the comment's moderation status to rejected, which removes it and its replies from public view; unhide publishes it again. The message keeps its place in the conversation and `hidden` flips on the returned message. A hidden comment no longer counts as unanswered. The account must have been connected with the moderation permission (Facebook `pages_manage_engagement`, Instagram `instagram_business_manage_comments`); 403 `reconnect_required` otherwise. Requires the `inbox:write` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "88231"
            },
            "description": "The inbox message id (from the conversation messages list)."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hide": {
                    "type": "boolean",
                    "default": true,
                    "description": "true to hide, false to unhide"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reply hidden or unhidden. On Facebook/Instagram, if the platform reports the comment no longer exists (its author deleted it, or it was already removed another way), this still returns 200 (there is nothing left to hide, and it stops counting as unanswered), but the response carries a `note` explaining that instead of silently implying the call hid something.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/InboxMessage"
                    },
                    "note": {
                      "type": "string",
                      "nullable": true,
                      "description": "Present only when the comment was already gone on the platform; explains that nothing was left to hide/unhide."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`unsupported_platform` (not an incoming comment on a supported platform) or `not_hideable` (Threads nested reply, or Threads refused)"
          },
          "401": {
            "description": "`reauth_required`: the Threads reply permission or the TikTok comments authorization is missing or expired"
          },
          "403": {
            "description": "`reconnect_required`: the account was connected without the comment-moderation permission; reconnect it in the dashboard"
          },
          "404": {
            "description": "`not_found` (message not in this workspace) or `account_not_connected`"
          },
          "422": {
            "description": "`cannot_hide`: Facebook does not let the Page hide (or unhide) this comment (the commenter blocked the Page, their account is deactivated or restricted, or the comment sits on a shared copy of the post). Retrying does not change it; handle it in Facebook, or mark the conversation read to skip it."
          },
          "429": {
            "description": "`quota_exceeded`: YouTube's daily API quota is used up; retry after midnight Pacific"
          },
          "502": {
            "description": "`platform_error`: the platform rejected the call. `hide_not_applied` (Instagram): Instagram accepted the hide or unhide but still reports the comment in the old state when read back, so the inbox row was left unchanged. This happens with comments Instagram shows under 'Comments from Facebook' on a reel that is also shared to Facebook (they live on Facebook); hide it in the Instagram or Facebook app, do not retry."
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/inbox/messages/98765/hide\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"hide\": true}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst result = await client.inbox.hide(\"98765\", { hide: true });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nresult = client.inbox.hide(\"98765\", hide=True)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:hide 98765"
          }
        ]
      }
    },
    "/inbox/messages/{id}": {
      "delete": {
        "summary": "Delete a comment",
        "description": "Deletes a comment someone left on one of your posts, on the platform and from the inbox. Facebook, Instagram and TikTok comments only: YouTube's API does not let a channel delete other people's comments, hide those instead (POST /inbox/messages/{id}/hide). Replies under the deleted comment go with it (the platforms cascade the delete and the inbox mirrors that); their ids are returned. A comment that is already gone on the platform is still removed from the inbox. This cannot be undone. Requires the `inbox:write` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string",
              "example": "88231"
            },
            "description": "The inbox message id (from the conversation messages list or GET /inbox/next)."
          }
        ],
        "responses": {
          "200": {
            "description": "Comment deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "The deleted message id",
                          "example": "88231"
                        },
                        "conversation_id": {
                          "type": "string",
                          "example": "instagram_comment_17912345678901234"
                        },
                        "removed_reply_ids": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "example": "88240"
                          },
                          "description": "Inbox ids of replies removed together with the comment"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`unsupported_platform`: not an incoming Facebook, Instagram or TikTok comment"
          },
          "401": {
            "description": "`reauth_required`: the TikTok comments authorization expired"
          },
          "403": {
            "description": "`reconnect_required`: the account was connected without the comment-moderation permission; reconnect it in the dashboard"
          },
          "404": {
            "description": "`not_found` (message not in this workspace) or `account_not_connected`"
          },
          "502": {
            "description": "`platform_error`: the platform rejected the call"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/inbox/messages/98765\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst result = await client.inbox.deleteMessage(\"98765\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nresult = client.inbox.delete_message(\"98765\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:delete 98765"
          }
        ]
      }
    },
    "/inbox/next": {
      "get": {
        "summary": "Get next conversation",
        "description": "A work queue for answering the inbox. Returns one item that still needs a reply, together with its conversation so far and the post it belongs to, so a reply can be drafted from one call. An item needs an answer when it is the customer's latest DM with no reply after it, or a comment/mention that has not been replied to and is not hidden. Order: DMs that can still be answered come first (Instagram/Facebook DMs inside Meta's 24-hour window, the one whose window closes soonest first, and X DMs), then Instagram/Facebook DMs whose window has closed (served with `reply_window.open = false`: answer them from the native app or mark them read), then comments and mentions, oldest first by default. Pass `message.id` as `message_id` to the reply endpoint so a comment reply lands under the served comment. Replies typed in the native apps count as answers (they are mirrored into the inbox), so a thread a colleague answered on their phone is not served again. Instagram mentions are skipped (no reply path). Only unread items are served by default: marking a conversation read (POST /inbox/conversations/{id}/read) is how to skip one for good; pass `include_read=true` to include read-but-unanswered items. Looks at the last 30 days of activity. To chain the queue, pass `include_next: true` to the reply endpoint and it returns the next item in the same response. Requires the `inbox:read` scope (API keys) or the `access_social_inbox` permission (OAuth/MCP).\n",
        "tags": [
          "Inbox"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "platform",
            "schema": {
              "type": "string",
              "enum": [
                "instagram",
                "facebook",
                "linkedin",
                "tiktok",
                "youtube",
                "x",
                "threads"
              ]
            },
            "description": "Only items from one platform."
          },
          {
            "in": "query",
            "name": "type",
            "schema": {
              "type": "string",
              "enum": [
                "dm",
                "comment",
                "mention"
              ]
            },
            "description": "Only items of one type."
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "oldest",
                "newest"
              ],
              "default": "oldest"
            },
            "description": "`oldest` serves the item that has waited longest first; `newest` the most recent."
          },
          {
            "in": "query",
            "name": "include_read",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Also serve items that were marked read but never answered."
          },
          {
            "in": "query",
            "name": "exclude",
            "schema": {
              "type": "string",
              "example": "instagram_comment_17912345678901234,instagram_comment_17912345678905678"
            },
            "description": "Comma-separated conversation ids to leave out of this call (a session-local skip; up to 100)."
          }
        ],
        "responses": {
          "200": {
            "description": "The next item, or null when nothing is waiting",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/InboxNextUnanswered"
                    },
                    "remaining": {
                      "type": "integer",
                      "example": 4,
                      "description": "Unanswered items still waiting after this one (capped at 500). 0 when `data` is null."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: unknown platform, type or order"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/inbox/next?platform=instagram\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst next = await client.inbox.next({ platform: \"instagram\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nitem = client.inbox.next(platform=\"instagram\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials inbox:next --platform instagram"
          }
        ]
      }
    },
    "/automations": {
      "get": {
        "summary": "List automations",
        "description": "Returns the comment-to-DM automations of the workspace, newest first. An automation answers a keyword in an Instagram comment (or a DM, or a story reply) with a DM, once per person. Needs the opt-in `automations:read` scope.",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "paused",
                "completed"
              ]
            }
          },
          {
            "in": "query",
            "name": "post_id",
            "schema": {
              "type": "string"
            },
            "description": "Only the automation bound to this OmniSocials post."
          }
        ],
        "responses": {
          "200": {
            "description": "List of automations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Automation"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/automations?status=active\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst automations = await client.automations.list({ status: \"active\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nautomations = client.automations.list(status=\"active\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:list --status active"
          }
        ]
      },
      "post": {
        "summary": "Create an automation",
        "description": "Creates a comment-to-DM automation on the connected Instagram account. When somebody comments a keyword on the post (or sends it in a DM / story reply, see `trigger.kinds`), the account sends them `actions.dm.text` as a private message, once per person, and optionally posts one of `actions.public_reply.variations` under the comment. Instagram rules: one private reply per comment, within 7 days of the comment; a follow-up is only possible after the person answers (24-hour window). OmniSocials caps sends at 180 DMs an hour per account. For `scope` `post`, pass `post_id` (an OmniSocials post that goes to Instagram; a scheduled post starts listening once it is live) or `instagram_media_id` (a post published outside OmniSocials). Needs the opt-in `automations:write` scope and a connected Instagram account.",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AutomationInput"
                  },
                  {
                    "type": "object",
                    "required": [
                      "actions"
                    ]
                  }
                ]
              },
              "examples": {
                "keywordOnOnePost": {
                  "summary": "Keyword on one post, with a public reply",
                  "value": {
                    "name": "Free guide",
                    "post_id": "12345",
                    "trigger": {
                      "keywords": [
                        "guide",
                        "link"
                      ],
                      "match": "word"
                    },
                    "actions": {
                      "dm": {
                        "text": "Here is the guide: https://example.com/guide"
                      },
                      "public_reply": {
                        "enabled": true,
                        "variations": [
                          "Sent you a DM!",
                          "Check your inbox 📩"
                        ]
                      }
                    },
                    "limits": {
                      "max_sends": 500
                    }
                  }
                },
                "nextPost": {
                  "summary": "The next post the workspace publishes",
                  "value": {
                    "trigger": {
                      "scope": "next_post",
                      "keywords": [
                        "price"
                      ]
                    },
                    "actions": {
                      "dm": {
                        "text": "The price list is here: https://example.com/prices"
                      }
                    }
                  }
                },
                "dmKeyword": {
                  "summary": "A keyword in any DM or story reply",
                  "value": {
                    "trigger": {
                      "kinds": [
                        "dm",
                        "story_reply"
                      ],
                      "keywords": [
                        "menu"
                      ]
                    },
                    "actions": {
                      "dm": {
                        "text": "Our menu: https://example.com/menu"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Automation created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Automation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (`validation_error`), or the post does not go to Instagram (`post_not_instagram`), or no post was given for the `post` scope (`post_required`)"
          },
          "404": {
            "description": "`post_not_found`, or `instagram_media_not_found` (Instagram has no such post on this account)"
          },
          "409": {
            "description": "`instagram_not_connected`, `instagram_needs_reauth`, or `automation_exists` (the post already has one; the response carries its `automation_id`)"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST \"https://api.omnisocials.com/v1/automations\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"post_id\": \"12345\",\n    \"trigger\": {\"keywords\": [\"guide\"]},\n    \"actions\": {\n      \"dm\": {\"text\": \"Here is the guide: https://example.com/guide\"}\n    }\n  }'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst automation = await client.automations.create({\n  post_id: \"12345\",\n  trigger: { keywords: [\"guide\"] },\n  actions: { dm: { text: \"Here is the guide: https://example.com/guide\" } },\n});"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nautomation = client.automations.create(\n    post_id=\"12345\",\n    trigger={\"keywords\": [\"guide\"]},\n    actions={\"dm\": {\"text\": \"Here is the guide: https://example.com/guide\"}},\n)"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:create --post-id 12345 --keywords guide --dm \"Here is the guide: https://example.com/guide\""
          }
        ]
      }
    },
    "/automations/{id}": {
      "get": {
        "summary": "Get an automation",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Automation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Automation"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Automation not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/automations/100000004\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst automation = await client.automations.get(\"100000004\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nautomation = client.automations.get(\"100000004\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:get 100000004"
          }
        ]
      },
      "patch": {
        "summary": "Update an automation",
        "description": "Partial update. `status` pauses (`paused`) or resumes (`active`) the automation. `trigger`, `actions` and `limits` are merged into the stored sections: keys you omit keep their value (new `keywords` keep the kinds, the scope and the match; a new `dm.text` keeps the public reply). `public_reply.variations: []` or `enabled: false` turns the public reply off; `limits.max_sends: null` removes that limit. Another `post_id` or `instagram_media_id` moves the automation to that post. A `completed` automation starts again when its limits or trigger change.",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutomationInput"
              },
              "examples": {
                "pause": {
                  "summary": "Pause",
                  "value": {
                    "status": "paused"
                  }
                },
                "newKeywords": {
                  "summary": "New keywords",
                  "value": {
                    "trigger": {
                      "keywords": [
                        "guide",
                        "pdf"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Automation updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Automation"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid update"
          },
          "404": {
            "description": "Automation not found"
          },
          "409": {
            "description": "`automation_exists`: the new post already has an automation"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X PATCH \"https://api.omnisocials.com/v1/automations/100000004\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"status\": \"paused\"}'"
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst automation = await client.automations.update(\"100000004\", { status: \"paused\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nautomation = client.automations.update(\"100000004\", status=\"paused\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:update 100000004 --pause"
          }
        ]
      },
      "delete": {
        "summary": "Delete an automation",
        "description": "Deletes the automation and its activity log. DMs that were already sent stay sent.",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Automation deleted"
          },
          "404": {
            "description": "Automation not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X DELETE \"https://api.omnisocials.com/v1/automations/100000004\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nawait client.automations.delete(\"100000004\");"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nclient.automations.delete(\"100000004\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:delete 100000004"
          }
        ]
      }
    },
    "/automations/{id}/events": {
      "get": {
        "summary": "List automation events",
        "description": "One row per comment or message the automation acted on, newest first: the DM that was sent, a skip with its reason, or a failure with the platform's error. `counts` gives the total per outcome.",
        "tags": [
          "Automations"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "outcome",
            "schema": {
              "type": "string",
              "enum": [
                "sent",
                "skipped",
                "failed",
                "pending"
              ]
            }
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Activity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AutomationEvent"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Rows per outcome, e.g. `{ \"sent\": 12, \"skipped\": 3 }`."
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Automation not found"
          }
        },
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl \"https://api.omnisocials.com/v1/automations/100000004/events?outcome=sent\" \\\n  -H \"Authorization: Bearer $OMNISOCIALS_API_KEY\""
          },
          {
            "lang": "js",
            "label": "Node.js",
            "source": "import { OmniSocials } from \"@omnisocials/sdk\";\n\nconst client = new OmniSocials(); // reads OMNISOCIALS_API_KEY\n\nconst events = await client.automations.listEvents(\"100000004\", { outcome: \"sent\" });"
          },
          {
            "lang": "python",
            "label": "Python",
            "source": "from omnisocials import OmniSocials\n\nclient = OmniSocials()  # reads OMNISOCIALS_API_KEY\n\nevents = client.automations.list_events(\"100000004\", outcome=\"sent\")"
          },
          {
            "lang": "bash",
            "label": "CLI",
            "source": "omnisocials automations:events 100000004 --outcome sent"
          }
        ]
      }
    }
  }
}
