{
  "info": {
    "name": "OmniSocials API",
    "description": "The OmniSocials REST API (1.0.0). Set the apiKey variable to your key (omsk_live_...). Docs: https://docs.omnisocials.com",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api.omnisocials.com/v1"
    },
    {
      "key": "apiKey",
      "value": "omsk_live_your_key"
    }
  ],
  "item": [
    {
      "name": "Posts",
      "item": [
        {
          "name": "List posts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "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.",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "20",
                  "description": "Number of posts to return",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "description": "Number of posts to skip",
                  "disabled": true
                }
              ]
            },
            "description": "List posts"
          }
        },
        {
          "name": "List live platform posts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/recent-platform",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                "recent-platform"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "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",
                  "disabled": true
                },
                {
                  "key": "platforms",
                  "value": "instagram,tiktok",
                  "description": "Optional comma-separated platform filter (e.g. \"instagram,tiktok\"). Defaults to every connected platform. Unsupported values return 400.\n",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Get a post",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024",
                  "description": "Post ID"
                }
              ]
            },
            "description": "Get a post"
          }
        },
        {
          "name": "Update a post",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/posts/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "description": "Update a post",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a post",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "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."
          }
        },
        {
          "name": "Create a post",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/posts/create",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                "create"
              ]
            },
            "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",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"content\": {\n    \"default\": \"Hello world!\",\n    \"instagram\": \"Hello from IG! #awesome\"\n  },\n  \"accounts\": [\n    \"instagram\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Create and publish post",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/posts/create-and-publish",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                "create-and-publish"
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"content\": {\n    \"default\": \"Hello world!\",\n    \"instagram\": \"Hello from IG! #awesome\"\n  },\n  \"accounts\": [\n    \"instagram\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Publish a post now",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id/publish",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id",
                "publish"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "description": "Publish a post now"
          }
        },
        {
          "name": "Retry failed post",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id/retry",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id",
                "retry"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "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."
          }
        },
        {
          "name": "Approve a post",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id/approve",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id",
                "approve"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "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."
          }
        },
        {
          "name": "Reject a post",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/posts/:id/reject",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id",
                "reject"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024"
                }
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get approval review",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/posts/:id/approval",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "posts",
                ":id",
                "approval"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1024",
                  "description": "Post ID"
                }
              ]
            },
            "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`."
          }
        }
      ]
    },
    {
      "name": "Media",
      "item": [
        {
          "name": "List media",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "20",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                },
                {
                  "key": "search",
                  "value": "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.",
                  "disabled": true
                },
                {
                  "key": "folder_id",
                  "value": "12",
                  "description": "Return only media in this folder. Use \"root\" for unfiled items at the top level.",
                  "disabled": true
                }
              ]
            },
            "description": "List media"
          }
        },
        {
          "name": "Get a media item",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "482913",
                  "description": "Media id returned by an upload endpoint."
                }
              ]
            },
            "description": "Fetch one media item by id. Use this to poll a large URL upload until its `status` becomes `ready` (or `failed`).\n"
          }
        },
        {
          "name": "Rename or move media",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/media/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "482913"
                }
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a media item",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "482913"
                }
              ]
            },
            "description": "Delete a media item"
          }
        },
        {
          "name": "Upload a file",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/upload",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "upload"
              ]
            },
            "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",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": ""
                },
                {
                  "key": "name",
                  "type": "text",
                  "value": "autumn-menu-hero"
                },
                {
                  "key": "folder",
                  "type": "text",
                  "value": "Autumn campaign"
                },
                {
                  "key": "folder_id",
                  "type": "text",
                  "value": "12"
                },
                {
                  "key": "pdf_mode",
                  "type": "text",
                  "value": ""
                }
              ]
            }
          }
        },
        {
          "name": "Upload from base64",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/media/upload-from-base64",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "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).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"data\": \"\",\n  \"mime_type\": \"image/jpeg\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Upload from a URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/media/upload-from-url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "upload-from-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",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/video.mp4\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Create an upload URL",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/create-upload-url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "create-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"
          }
        },
        {
          "name": "Split a PDF into pages",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/:id/split",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                ":id",
                "split"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "482913"
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Check media limits",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/media/check",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "check"
              ]
            },
            "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",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Upload with a token",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/media/upload-by-token",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "media",
                "upload-by-token"
              ],
              "query": [
                {
                  "key": "upload_token",
                  "value": "9f2c4e1ab07d",
                  "description": "The token from POST /media/create-upload-url.",
                  "disabled": false
                }
              ]
            },
            "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",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "src": ""
                },
                {
                  "key": "name",
                  "type": "text",
                  "value": "autumn-menu-hero"
                },
                {
                  "key": "pdf_mode",
                  "type": "text",
                  "value": ""
                }
              ]
            }
          }
        }
      ]
    },
    {
      "name": "Folders",
      "item": [
        {
          "name": "List folders",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/folders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "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."
          }
        },
        {
          "name": "Create a folder",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/folders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "folders"
              ]
            },
            "description": "Create a folder",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Rename or move a folder",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/folders/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "folders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Rename or move a folder",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a folder",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/folders/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "folders",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "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."
          }
        }
      ]
    },
    {
      "name": "Hashtag Sets",
      "item": [
        {
          "name": "List hashtag sets",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/hashtag-sets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "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."
          }
        },
        {
          "name": "Create a hashtag set",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/hashtag-sets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "hashtag-sets"
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\",\n  \"hashtags\": [\n    \"\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get a hashtag set",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/hashtag-sets/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "hashtag-sets",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Get a hashtag set"
          }
        },
        {
          "name": "Update a hashtag set",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/hashtag-sets/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "hashtag-sets",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Rename the set and/or replace its hashtags. `hashtags` replaces the full list (fetch, modify, resend to add/remove tags).",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a hashtag set",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/hashtag-sets/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "hashtag-sets",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Deletes the set. Posts that already used it are unaffected. The tags were merged into their captions at create time."
          }
        }
      ]
    },
    {
      "name": "Accounts",
      "item": [
        {
          "name": "List accounts",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/accounts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "accounts"
              ],
              "query": [
                {
                  "key": "platform",
                  "value": "",
                  "description": "Filter by platform",
                  "disabled": true
                }
              ]
            },
            "description": "Lists the social accounts connected to the workspace of this API key, one entry per channel. Requires the `accounts:read` scope."
          }
        },
        {
          "name": "Get an account",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/accounts/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "accounts",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "1234_instagram",
                  "description": "Account ID (format workspaceId_platform)"
                }
              ]
            },
            "description": "Get an account"
          }
        }
      ]
    },
    {
      "name": "Analytics",
      "item": [
        {
          "name": "Get bulk post analytics",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/analytics/posts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "analytics",
                "posts"
              ],
              "query": [
                {
                  "key": "ids",
                  "value": "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",
                  "disabled": false
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Get post analytics",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/analytics/posts/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "analytics",
                "posts",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "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."
                }
              ]
            },
            "description": "Get post analytics"
          }
        },
        {
          "name": "Get analytics overview",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/analytics/overview",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "analytics",
                "overview"
              ],
              "query": [
                {
                  "key": "period",
                  "value": "30d",
                  "description": "Time period for analytics",
                  "disabled": true
                },
                {
                  "key": "start_date",
                  "value": "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.",
                  "disabled": true
                },
                {
                  "key": "end_date",
                  "value": "2026-09-30",
                  "description": "Custom end date. Same formats as start_date; \"YYYY-MM\" expands to the last day of the month.",
                  "disabled": true
                }
              ]
            },
            "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."
          }
        },
        {
          "name": "Get account analytics",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/analytics/accounts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "analytics",
                "accounts"
              ],
              "query": [
                {
                  "key": "platform",
                  "value": "instagram",
                  "description": "Filter by platform",
                  "disabled": true
                },
                {
                  "key": "date",
                  "value": "2026-10-20",
                  "description": "Date to get analytics for (YYYY-MM-DD, defaults to today)",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Get best posting times",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/analytics/best-times",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "analytics",
                "best-times"
              ],
              "query": [
                {
                  "key": "platform",
                  "value": "instagram",
                  "description": "Platform identifier (e.g. instagram, tiktok, linkedin, linkedin_page, x).",
                  "disabled": false
                },
                {
                  "key": "timezone",
                  "value": "Europe/Amsterdam",
                  "description": "IANA timezone for the buckets (e.g. Europe/Amsterdam). Defaults to the account's timezone, then UTC.",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        }
      ]
    },
    {
      "name": "Approvals",
      "item": [
        {
          "name": "List approval workflows",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/approval-workflows",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "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."
          }
        }
      ]
    },
    {
      "name": "Webhooks",
      "item": [
        {
          "name": "List webhooks",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks"
              ]
            },
            "description": "List webhooks"
          }
        },
        {
          "name": "Create a webhook",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks"
              ]
            },
            "description": "Create a webhook",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/webhooks/omnisocials\",\n  \"events\": [\n    \"post.scheduled\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get a webhook",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "74327072-ec54-473c-837f-63b29187ac16"
                }
              ]
            },
            "description": "Get a webhook"
          }
        },
        {
          "name": "Update a webhook",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "74327072-ec54-473c-837f-63b29187ac16"
                }
              ]
            },
            "description": "Update a webhook",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a webhook",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/webhooks/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "74327072-ec54-473c-837f-63b29187ac16"
                }
              ]
            },
            "description": "Delete a webhook"
          }
        },
        {
          "name": "Rotate webhook secret",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/webhooks/:id/rotate-secret",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "webhooks",
                ":id",
                "rotate-secret"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "74327072-ec54-473c-837f-63b29187ac16"
                }
              ]
            },
            "description": "Rotate webhook secret"
          }
        }
      ]
    },
    {
      "name": "Locations",
      "item": [
        {
          "name": "Search locations",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/locations/search",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "locations",
                "search"
              ],
              "query": [
                {
                  "key": "platform",
                  "value": "instagram",
                  "description": "Which platform the location will be tagged on. Determines the search source and the id type returned.",
                  "disabled": true
                },
                {
                  "key": "q",
                  "value": "Griffith Observatory",
                  "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`.",
                  "disabled": true
                },
                {
                  "key": "latitude",
                  "value": "34.1184",
                  "description": "Threads only. Search places around this point; pass together with `longitude` instead of `q`.",
                  "disabled": true
                },
                {
                  "key": "longitude",
                  "value": "-118.3004",
                  "description": "Threads only. Pass together with `latitude`.",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Validate a location",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/locations/validate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "locations",
                "validate"
              ],
              "query": [
                {
                  "key": "id",
                  "value": "313813441",
                  "description": "Numeric Facebook Place ID to validate.",
                  "disabled": false
                }
              ]
            },
            "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."
          }
        }
      ]
    },
    {
      "name": "Audio",
      "item": [
        {
          "name": "Search Instagram audio",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/audio/search",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "audio",
                "search"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "birthday",
                  "description": "Song title, artist, or keyword. Omit for trending audio.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "music",
                  "description": "Catalog to search: licensed music (default) or original sounds created on Instagram.",
                  "disabled": true
                }
              ]
            },
            "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)."
          }
        }
      ]
    },
    {
      "name": "Pinterest",
      "item": [
        {
          "name": "List product Pins",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/pinterest/products",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "pinterest",
                "products"
              ],
              "query": [
                {
                  "key": "source",
                  "value": "",
                  "description": "Where to read product Pins from. Default: `catalog` when the connection has catalog access, else `pins`.",
                  "disabled": true
                },
                {
                  "key": "product_group_id",
                  "value": "443727193917",
                  "description": "Catalog source only. A product group `id` from `product_groups`. Default: the group named \"All Products\", else the first group.",
                  "disabled": true
                },
                {
                  "key": "bookmark",
                  "value": "",
                  "description": "Cursor from the previous response, to get the next page.",
                  "disabled": true
                },
                {
                  "key": "page_size",
                  "value": "25",
                  "description": "Catalog source only. Products per page.",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Check a product Pin",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/pinterest/products/validate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "pinterest",
                "products",
                "validate"
              ],
              "query": [
                {
                  "key": "id",
                  "value": "813744226420795884",
                  "description": "Pin ID or Pin link of the product Pin.",
                  "disabled": false
                }
              ]
            },
            "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."
          }
        }
      ]
    },
    {
      "name": "Inbox",
      "item": [
        {
          "name": "List conversations",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/inbox/conversations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "conversations"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "25",
                  "description": "Max conversations per page (1-100, default 25).",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "MjAyNi0xMC0yMVQwOTowMDowMC4wMDBafDg4MjMx",
                  "description": "Opaque pagination cursor from a previous response.",
                  "disabled": true
                },
                {
                  "key": "platform",
                  "value": "",
                  "description": "Filter to a single platform.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "",
                  "description": "Filter to a single conversation type.",
                  "disabled": true
                },
                {
                  "key": "unread",
                  "value": "true",
                  "description": "When true, only conversations with unread incoming messages.",
                  "disabled": true
                },
                {
                  "key": "unanswered",
                  "value": "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",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "List messages",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/inbox/conversations/:conversationId/messages",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "conversations",
                ":conversationId",
                "messages"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50",
                  "disabled": true
                },
                {
                  "key": "cursor",
                  "value": "MjAyNi0xMC0yMVQwOTowMDowMC4wMDBafDg4MjMx",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "conversationId",
                  "value": "instagram_comment_17912345678901234"
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Mark conversation read",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/inbox/conversations/:conversationId/read",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "conversations",
                ":conversationId",
                "read"
              ],
              "variable": [
                {
                  "key": "conversationId",
                  "value": "instagram_comment_17912345678901234"
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Reply to a conversation",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/inbox/conversations/:conversationId/reply",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "conversations",
                ":conversationId",
                "reply"
              ],
              "variable": [
                {
                  "key": "conversationId",
                  "value": "instagram_comment_17912345678901234"
                }
              ]
            },
            "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",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Hide or unhide a comment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/inbox/messages/:id/hide",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "messages",
                ":id",
                "hide"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "88231",
                  "description": "The inbox message id (from the conversation messages list)."
                }
              ]
            },
            "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",
            "body": {
              "mode": "raw",
              "raw": "{}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete a comment",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/inbox/messages/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "messages",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": "88231",
                  "description": "The inbox message id (from the conversation messages list or GET /inbox/next)."
                }
              ]
            },
            "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"
          }
        },
        {
          "name": "Get next conversation",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/inbox/next",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "inbox",
                "next"
              ],
              "query": [
                {
                  "key": "platform",
                  "value": "",
                  "description": "Only items from one platform.",
                  "disabled": true
                },
                {
                  "key": "type",
                  "value": "",
                  "description": "Only items of one type.",
                  "disabled": true
                },
                {
                  "key": "order",
                  "value": "oldest",
                  "description": "`oldest` serves the item that has waited longest first; `newest` the most recent.",
                  "disabled": true
                },
                {
                  "key": "include_read",
                  "value": "false",
                  "description": "Also serve items that were marked read but never answered.",
                  "disabled": true
                },
                {
                  "key": "exclude",
                  "value": "instagram_comment_17912345678901234,instagram_comment_17912345678905678",
                  "description": "Comma-separated conversation ids to leave out of this call (a session-local skip; up to 100).",
                  "disabled": true
                }
              ]
            },
            "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"
          }
        }
      ]
    },
    {
      "name": "Automations",
      "item": [
        {
          "name": "List automations",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/automations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "post_id",
                  "value": "",
                  "description": "Only the automation bound to this OmniSocials post.",
                  "disabled": true
                }
              ]
            },
            "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."
          }
        },
        {
          "name": "Create an automation",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/automations",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations"
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Free guide\",\n  \"post_id\": \"12345\",\n  \"trigger\": {\n    \"keywords\": [\n      \"guide\",\n      \"link\"\n    ],\n    \"match\": \"word\"\n  },\n  \"actions\": {\n    \"dm\": {\n      \"text\": \"Here is the guide: https://example.com/guide\"\n    },\n    \"public_reply\": {\n      \"enabled\": true,\n      \"variations\": [\n        \"Sent you a DM!\",\n        \"Check your inbox 📩\"\n      ]\n    }\n  },\n  \"limits\": {\n    \"max_sends\": 500\n  }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Get an automation",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/automations/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Get an automation"
          }
        },
        {
          "name": "Update an automation",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/automations/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "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.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"paused\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Delete an automation",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/automations/:id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations",
                ":id"
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "description": "Deletes the automation and its activity log. DMs that were already sent stay sent."
          }
        },
        {
          "name": "List automation events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/automations/:id/events",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "automations",
                ":id",
                "events"
              ],
              "query": [
                {
                  "key": "outcome",
                  "value": "",
                  "disabled": true
                },
                {
                  "key": "limit",
                  "value": "25",
                  "disabled": true
                },
                {
                  "key": "offset",
                  "value": "0",
                  "disabled": true
                }
              ],
              "variable": [
                {
                  "key": "id",
                  "value": ""
                }
              ]
            },
            "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."
          }
        }
      ]
    }
  ]
}
