{
  "openapi": "3.1.0",
  "info": {
    "title": "Multi Upload Tool API",
    "version": "1.0.0",
    "summary": "Publish and schedule video and photo posts to TikTok, YouTube, Instagram, Facebook, LinkedIn, Pinterest, Bluesky and Threads from one API.",
    "description": "REST API for the Multi Upload Tool platform.\n\nUpload a video or photo once and publish it to one or many connected social accounts, immediately or on a schedule. The API also covers connected-account management, upload history, per-account daily limits, webhooks, teams, white-label connection links, short links and custom domains.\n\n## Authentication\n\nEvery endpoint requires an API token sent in the `x-api-key` header. Tokens are created from the dashboard and look like `kmb_<64 hex characters>`.\n\n```\nx-api-key: kmb_0123456789abcdef...\n```\n\n## Permission model\n\nA token carries **scopes**, chosen when it is created in the dashboard. `*` grants everything: it is what every token created before scopes existed holds, and what a token created without a list gets. Otherwise the token holds `resource:action` scopes — `posts:read`, `posts:write`, `posts:publish`, `accounts:read`, `accounts:write`, `analytics:read`, `pools:read`, `pools:write`, `webhooks:read`, `webhooks:write`, `team:read`, `team:write`, `links:read`, `links:write`. `write` includes `read`, and `posts:publish` includes `posts:write`. A token with `posts:write` but not `posts:publish` can create posts, but they are always held for approval. Each operation states what it needs in `x-required-scope`; a call without it returns `403` with `code: INSUFFICIENT_SCOPE` and `requiredScope`.\n\nOn top of scopes, **team scoping** applies.\n\n- A token created against a team is permanently pinned to that team. It can only read and write that team's resources, and it is revoked automatically if the user leaves the team.\n- An unscoped token may target a team per-request with the `X-Team-ID` header. Membership is verified on every call.\n- Inside a team, the member's role (`OWNER`, `ADMIN`, `MEMBER`) gates privileged operations. Members cannot create webhooks; only owners and admins can invite or remove members; only an owner can remove an admin.\n\nTreat an API token as a credential and store it accordingly; give each integration only the scopes it needs.\n\n## Large media\n\nA file part sent to `POST /upload` must stay under 100 MB, an edge limit on the request body rather than a platform one. For anything larger, call `POST /upload/presign` to get URLs that put the bytes straight into storage, then publish by passing the returned `bucketUrl` (preferred for large video) or `accessUrl` in the `video`, `photo` or `file` field.\n\n## Rate limits\n\nLimits are applied per IP and per endpoint group; exceeding one returns `429`. The tightest limits are on writes: 50 requests / 15 min for `POST /upload`, 20 requests / 15 min for `POST /upload/bulk` and for destructive operations. Reads are typically 100 requests / 5-15 min. A single IP may also present at most 10 distinct API tokens per hour.\n\n## Errors\n\nErrors return a JSON body of the form `{ \"success\": false, \"error\": \"...\" }`. Validation failures on `POST /upload` additionally return a per-field `errors` array.",
    "termsOfService": "https://multi-upload-tool.com/terms",
    "contact": {
      "name": "Multi Upload Tool support",
      "url": "https://multi-upload-tool.com/about",
      "email": "support@multi-upload-tool.com"
    }
  },
  "externalDocs": {
    "description": "Full API documentation",
    "url": "https://docs.multi-upload-tool.com/api-reference"
  },
  "servers": [
    {
      "url": "https://api.multi-upload-tool.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "Accounts", "description": "Connected social media accounts." },
    { "name": "Uploads", "description": "Publish posts and inspect upload history." },
    { "name": "Limits", "description": "Per-account daily publishing limits." },
    { "name": "Webhooks", "description": "Event notifications delivered to your endpoint." },
    { "name": "Teams", "description": "Team membership and invitations." },
    { "name": "White-label", "description": "Hosted connection links for your own users." },
    { "name": "Short links", "description": "Trackable short links with routing rules." },
    { "name": "Domains", "description": "Custom domains for short links." },
    { "name": "Pools", "description": "Queues of media drip-fed to accounts." },
    { "name": "Analytics", "description": "Views, likes, comments and shares collected nightly for every connected account." }
  ],
  "paths": {
    "/accounts": {
      "get": {
        "tags": ["Accounts"],
        "summary": "List connected accounts",
        "description": "Returns every social account connected to the caller, or to the team when the request is team-scoped.",
        "operationId": "listAccounts",
        "x-required-scope": "accounts:read",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Only return accounts on this platform.",
            "schema": { "$ref": "#/components/schemas/Platform" }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only return accounts in this connection state.",
            "schema": { "$ref": "#/components/schemas/AccountStatus" }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Only return accounts whose tag list contains this substring.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching connected accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data", "count"],
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Account" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/accounts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Connected account identifier.",
          "schema": { "type": "string" }
        }
      ],
      "get": {
        "tags": ["Accounts"],
        "summary": "Get a connected account",
        "description": "Returns a single connected account, including its current connection state.",
        "operationId": "getAccount",
        "x-required-scope": "accounts:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The connected account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data"],
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Account" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Accounts"],
        "summary": "Disconnect an account",
        "description": "Revokes the stored credentials and removes the account. Already published posts are unaffected.",
        "operationId": "deleteAccount",
        "x-required-scope": "accounts:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/accounts/linkedin/pages": {
      "get": {
        "tags": ["Accounts"],
        "summary": "List LinkedIn pages",
        "description": "Returns the LinkedIn organization pages the connected account can publish to.",
        "operationId": "listLinkedInPages",
        "x-required-scope": "accounts:read",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          {
            "name": "accountId",
            "in": "query",
            "required": true,
            "description": "A connected LinkedIn account.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The organization pages available to that account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload": {
      "get": {
        "tags": ["Uploads"],
        "summary": "List uploads",
        "description": "Returns upload history, most recent first.",
        "operationId": "listUploads",
        "x-required-scope": "posts:read",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/UploadStatus" }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/Platform" }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Free-text match against the upload's title and description.",
            "schema": { "type": "string" }
          },
          {
            "name": "startDate",
            "in": "query",
            "required": false,
            "description": "Only uploads from this instant onward.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "endDate",
            "in": "query",
            "required": false,
            "description": "Only uploads up to this instant.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of uploads to return.",
            "schema": { "type": "integer", "minimum": 1 }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "The matching uploads.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data", "pagination"],
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Upload" } },
                    "pagination": { "$ref": "#/components/schemas/Pagination" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Uploads"],
        "summary": "Publish to one account",
        "description": "Publishes a video, photo or text post to a single connected account.\n\nMedia can be sent either as a file part or as a publicly reachable URL string in the same field. Platforms that accept text-only posts (Facebook, LinkedIn, Bluesky, Threads) may omit media entirely.\n\nA file part sent here must stay under 100 MB: the request body is capped at that size by our edge and a bigger one is rejected with `413` before it reaches the API. That cap is an edge limit, not a platform one - to publish larger media, get a URL from `POST /upload/presign` and send it in the same field.\n\nThe request returns as soon as the job is queued. Supply `schedule_date` (or `scheduledDate` for YouTube) to publish later. Platform-specific fields are ignored by platforms that do not support them.",
        "operationId": "createUpload",
        "x-required-scope": "posts:write",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          { "$ref": "#/components/parameters/IdempotencyKey" },
          {
            "name": "async",
            "in": "query",
            "required": false,
            "description": "Set to `false` to wait for the post to finish publishing instead of returning once queued.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/UploadRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The post was queued, scheduled, or (when `async=false`) published.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "message", "data"],
                  "properties": {
                    "success": { "const": true },
                    "message": { "type": "string" },
                    "data": { "$ref": "#/components/schemas/QueuedUpload" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "The account does not exist, is inactive, or is not owned by the caller.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "409": {
            "description": "A request with the same `Idempotency-Key` is still in progress. Retry later.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Rate limit exceeded, or the account reached its daily platform limit.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/upload/bulk": {
      "post": {
        "tags": ["Uploads"],
        "summary": "Publish to many accounts",
        "description": "Publishes the same media to several connected accounts in one request. One upload job is created per account, so a failure on one account does not affect the others.",
        "operationId": "createBulkUpload",
        "x-required-scope": "posts:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }, { "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/BulkUploadRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One result per requested account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "message": { "type": "string" },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/QueuedUpload" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": {
            "description": "A request with the same `Idempotency-Key` is still in progress. Retry later.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/validate": {
      "post": {
        "tags": ["Uploads"],
        "summary": "Validate a post (dry run)",
        "description": "Runs every check `POST /upload/bulk` would run (account status and ownership, plan, monthly quota, daily cap, queue room, caption and title limits, required media and platform settings) and publishes nothing: no upload is created, nothing is queued, no file is stored. Takes the same body as `/upload/bulk`. Media content (format, duration) is only checked when the post is actually published.",
        "operationId": "validateUpload",
        "x-required-scope": "posts:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": { "$ref": "#/components/schemas/BulkUploadRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The verdict. `valid` is true only when every account would accept the post. An error with a null `accountId` applies to the whole request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "valid", "errors", "targets"],
                  "properties": {
                    "success": { "const": true },
                    "valid": { "type": "boolean" },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["accountId", "platform", "error"],
                        "properties": {
                          "accountId": { "type": ["string", "null"] },
                          "platform": { "type": ["string", "null"] },
                          "error": { "type": "string" }
                        }
                      }
                    },
                    "targets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["accountId", "platform", "scheduledFor"],
                        "properties": {
                          "accountId": { "type": "string" },
                          "platform": { "type": ["string", "null"] },
                          "scheduledFor": { "type": ["string", "null"], "format": "date-time" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/platforms/capabilities": {
      "get": {
        "tags": ["Limits"],
        "summary": "Platform capabilities",
        "description": "Caption and title limits, daily post cap, accepted media and required settings for each platform, as data. Without `platform`, every platform is returned as an array. A null limit means the platform has no such field.",
        "operationId": "getPlatformCapabilities",
        "x-required-scope": "posts:read",
        "parameters": [
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Return one platform only.",
            "schema": { "$ref": "#/components/schemas/Platform" }
          }
        ],
        "responses": {
          "200": {
            "description": "One capabilities object, or an array of them when `platform` is omitted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "oneOf": [
                        { "type": "object" },
                        { "type": "array", "items": { "type": "object" } }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "description": "Unknown platform." },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/presign": {
      "post": {
        "tags": ["Uploads"],
        "summary": "Presign a large media upload",
        "description": "Creates presigned URLs so large media is uploaded straight to object storage instead of through this API.\n\n`POST /upload` accepts the file itself, but its request body is capped at **100 MB by our edge**: a bigger multipart request is rejected with `413` before it ever reaches the API. That ceiling is an edge limit, not a platform one. The real per-media ceilings are the `maxFileSize` of the presign route you choose (up to 10 GiB) and, at publication time, the target platform's own limits.\n\nNothing changes for files under 100 MB: keep sending them to `POST /upload` as a file part exactly as before. Presign only what is too big for that path.\n\n### Phase 1 - presign\n\nDeclare each file's exact byte length; it is signed into the URLs you get back.\n\n```bash\ncurl -X POST \"https://api.multi-upload-tool.com/api/v1/upload/presign\" \\\n  -H \"x-api-key: kmb_YOUR_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"route\":\"tiktok_videos\",\"files\":[{\"name\":\"clip.mp4\",\"size\":734003200,\"type\":\"video/mp4\"}]}'\n```\n\n### Phase 2 - upload the bytes\n\nThey travel directly to storage and never touch this API or its edge, which is what removes the 100 MB ceiling.\n\n**Single-request routes** (`photos`, `instagram_photos`, `threads_photos`, `x_photos`, `bluesky_photos`, `thumbnails`, `documents`) return `uploadUrl` and `uploadHeaders`. `PUT` the whole file to `uploadUrl`, sending exactly the headers returned: `content-type` and `content-length` are part of the signature, so changing or omitting either fails the `PUT` with `SignatureDoesNotMatch`. Valid for 1 hour.\n\n**Multipart routes** (`youtube_videos`, `tiktok_videos`, `pinterest_videos`, `threads_videos`, `x_videos`, `linkedin_videos`) return `multipart`. For each entry of `multipart.parts`, `PUT` the byte range starting at `(partNumber - 1) * partSize` and `size` bytes long to that part's `uploadUrl`, with `Content-Length` equal to that part's `size`. Keep each response's `ETag` header, quotes stripped. Parts may be uploaded in parallel, and a single failed part can be retried by re-`PUT`ting the same URL. Once every part is stored, `POST` to `multipart.completeUrl` with `Content-Type: application/xml` and the parts sorted ascending:\n\n```xml\n<CompleteMultipartUpload><Part><ETag>abc123</ETag><PartNumber>1</PartNumber></Part><Part><ETag>def456</ETag><PartNumber>2</PartNumber></Part></CompleteMultipartUpload>\n```\n\nIf you abandon the upload, send `DELETE` to `multipart.abortUrl`: parts left behind are stored and billed and are not cleaned up for you. Part and complete URLs last 6 hours and cannot be refreshed, so upload parts concurrently rather than one after another, and presign again if the window lapses.\n\n### Phase 3 - publish\n\nEach presigned file comes back with two URLs for the same object. Pass one of them as the `video`, `photo` or `file` field of `POST /upload` - those fields already accept a URL string - or of `POST /upload/bulk` to fan the same media out across accounts.\n\n- **`bucketUrl`** - use it for large video. The API reads the object straight from storage, so the file is never streamed back through this API and its edge on the way to the platform. It points at private storage and is not fetchable on its own.\n- **`accessUrl`** - use it when you also need a URL anyone can fetch, and for photos. It is served from a domain the platforms have verified, which is what lets TikTok and the other pull-based platforms fetch the media. Pass it through unchanged; a rehosted copy on your own domain is rejected by those platforms.\n\n```bash\ncurl -X POST \"https://api.multi-upload-tool.com/api/v1/upload\" \\\n  -H \"x-api-key: kmb_YOUR_TOKEN\" \\\n  -F \"accountId=101\" \\\n  -F \"video=https://multi-upload-tools.s3.eu-central-003.backblazeb2.com/public-api/42/1756226400000-a1b2c3d4-clip.mp4\" \\\n  -F \"title=My Video\"\n```\n\n### Routes\n\n`route` selects the validation preset applied to the files you declare. It does not tie the media to a platform - any presigned object may be published to any account - but choosing the preset that matches your target rejects an oversized or wrong-typed file here, instead of at publication time where the platform's own limits still apply.\n\n| route | multipart | max files | max size per file | accepted types |\n|---|---|---|---|---|\n| `youtube_videos` | yes | 1 | 10 GiB | `video/*`, `application/octet-stream` |\n| `tiktok_videos` | yes | 1 | 4 GiB | `video/mp4`, `video/webm` |\n| `pinterest_videos` | yes | 1 | 2 GiB | `video/mp4`, `video/quicktime`, `video/x-m4v` |\n| `threads_videos` | yes | 1 | 1 GiB | `video/mp4`, `video/quicktime` |\n| `x_videos` | yes | 1 | 1 GiB | `video/mp4`, `video/quicktime` |\n| `linkedin_videos` | yes | 1 | 500 MiB | `video/mp4` |\n| `documents` | no | 1 | 100 MiB | PDF, PPT, PPTX, DOC, DOCX |\n| `photos` | no | 35 | 20 MiB | `image/jpeg`, `image/png`, `image/webp` |\n| `thumbnails` | no | 1 | 20 MiB | `image/jpeg`, `image/png`, `image/webp`, `image/gif` |\n| `instagram_photos` | no | 10 | 8 MiB | `image/jpeg`, `image/png`, `image/webp` |\n| `threads_photos` | no | 20 | 8 MiB | `image/jpeg`, `image/png` |\n| `x_photos` | no | 4 | 5 MiB | `image/jpeg`, `image/png`, `image/webp`, `image/gif` |\n| `bluesky_photos` | no | 4 | 2 000 000 bytes | `image/jpeg`, `image/png`, `image/webp`, `image/gif` |\n\nA declared `size` of `0` is rejected: a zero-byte multipart upload can never be completed.\n\n### Quota and rate limit\n\nPresigning is metered per user rather than per IP, so a fleet of workers behind one egress address is not throttled as if it were one client, and one busy token cannot spend another customer's allowance. The limit is **120 requests / 15 min**; exceeding it returns `429` with `retryAfter` in seconds. A far looser per-IP backstop of 600 requests / 15 min also applies, and answers `429` the same way.\n\nThe request body only declares files, so it is capped at 256 KB - a bigger one is refused with `413` and `code: BODY_TOO_LARGE` without being read.\n\nThe bytes you declare are charged against a rolling 30-day allowance that depends on your plan. Every successful response carries a `usage` block - what is used including this request, the allowance, and what is left - so you can see the headroom without failing first. Exceeding it returns `403` with `code: STORAGE_QUOTA_EXCEEDED`. The debit happens when the URLs are issued, using the sizes you declared, and is not refunded if you never upload the bytes: presigning and walking away spends the allowance. Plans with no configured allowance report `\"unlimited\": true` and are never blocked.",
        "operationId": "presignUpload",
        "x-required-scope": "posts:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PresignRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One presigned entry per declared file, plus the caller's current storage usage.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PresignResponse" }
              }
            }
          },
          "400": {
            "description": "The body is not valid JSON (`INVALID_JSON`), a declared file is missing an exact non-zero `size` (`INVALID_REQUEST`), or the route rejected the files: `TOO_MANY_FILES`, `FILE_TOO_LARGE`, `INVALID_FILE_TYPE`, `REJECTED`. Failures caused by the route echo its `limits`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PresignError" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The rolling storage allowance would be exceeded (`STORAGE_QUOTA_EXCEEDED`, with a `usage` block), or the request is not permitted inside the requested team.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PresignError" }
              }
            }
          },
          "404": {
            "description": "Unknown `route` (`UNKNOWN_ROUTE`). The response lists the accepted names in `validRoutes`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PresignError" }
              }
            }
          },
          "413": {
            "description": "The request body is larger than 256 KB (`BODY_TOO_LARGE`). This endpoint only declares files; the bytes themselves go to the presigned URLs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PresignError" }
              }
            }
          },
          "429": {
            "description": "More than 120 presign requests in 15 minutes for this user, or more than 600 from this IP. `retryAfter` gives the seconds to wait.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    { "$ref": "#/components/schemas/Error" },
                    {
                      "type": "object",
                      "properties": { "retryAfter": { "type": "integer", "description": "Seconds until the window resets." } }
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "The rate limiter or the quota counter could not be reached. No URLs were issued and nothing was charged; retry.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/upload/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Upload identifier.",
          "schema": { "type": "integer" }
        }
      ],
      "get": {
        "tags": ["Uploads"],
        "summary": "Get an upload",
        "description": "Returns the current state of a single upload, including the published post URL once it succeeds.",
        "operationId": "getUpload",
        "x-required-scope": "posts:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The upload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Upload" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Uploads"],
        "summary": "Reschedule an upload",
        "description": "Changes the scheduled publication time of an upload that has not been published yet.",
        "operationId": "updateUpload",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scheduledFor": {
                    "type": "string",
                    "format": "date-time",
                    "description": "New publication time, ISO 8601."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated upload.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Upload" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/{id}/retry": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Upload identifier.",
          "schema": { "type": "integer" }
        }
      ],
      "post": {
        "tags": ["Uploads"],
        "summary": "Retry a failed upload",
        "description": "Queues one failed upload again. An upload is one account of a post, so the accounts that were published are never touched. Refused when retrying cannot help (`failure.retryable` false) unless `force` is true, when the account is disconnected, when the upload is still being retried automatically, or when its media was deleted (24 hours after the failure).",
        "operationId": "retryUpload",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": { "type": "boolean", "description": "Retry even when `failure.retryable` is false." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The upload is queued again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "type": "object",
                      "properties": { "uploadId": { "type": "integer" }, "status": { "const": "pending" } }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": {
            "description": "No such upload in your scope.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "409": {
            "description": "The upload cannot be retried. `code` says why: NOT_FAILED, ALREADY_RETRYING, NOT_RETRYABLE, ACCOUNT_INACTIVE, MEDIA_EXPIRED, DAILY_LIMIT, PLAN_LIMIT, QUEUE_FULL or QUEUE_BUSY.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/retry-failed": {
      "post": {
        "tags": ["Uploads"],
        "summary": "Retry all failed uploads",
        "description": "Queues again every retryable upload that failed in the last 24 hours, optionally for one account or platform. Published uploads are never touched. Uploads that cannot be retried are listed in `skipped` with the reason.",
        "operationId": "retryFailedUploads",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "accountId": { "type": "string" },
                  "platform": { "$ref": "#/components/schemas/Platform" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was queued again and what was skipped.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "retried": { "type": "array", "items": { "type": "integer" } },
                        "skipped": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "uploadId": { "type": "integer" },
                              "platform": { "$ref": "#/components/schemas/Platform" },
                              "code": { "type": "string" },
                              "reason": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/{id}/submit": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "description": "Upload identifier.", "schema": { "type": "integer" } }
      ],
      "post": {
        "tags": ["Uploads"],
        "summary": "Submit a draft for approval",
        "description": "Moves a draft to `pending_approval`.",
        "operationId": "submitUpload",
        "x-required-scope": "posts:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": { "description": "The post.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "uploadId": { "type": "integer" }, "status": { "$ref": "#/components/schemas/UploadStatus" }, "scheduledFor": { "type": ["string", "null"], "format": "date-time" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Only a team owner or admin can do this.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "The post is not in a state that allows it (`code`: NOT_HELD, NOT_DRAFT, ACCOUNT_INACTIVE, DAILY_LIMIT, QUEUE_FULL).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/{id}/approve": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "description": "Upload identifier.", "schema": { "type": "integer" } }
      ],
      "post": {
        "tags": ["Uploads"],
        "summary": "Approve a post",
        "description": "Owner or admin only. Schedules a draft or a post awaiting approval at its date, or publishes it now if it has none or the date has passed.",
        "operationId": "approveUpload",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": { "description": "The post, now scheduled or queued.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "uploadId": { "type": "integer" }, "status": { "$ref": "#/components/schemas/UploadStatus" }, "scheduledFor": { "type": ["string", "null"], "format": "date-time" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Only a team owner or admin can do this.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "The post is not in a state that allows it (`code`: NOT_HELD, NOT_DRAFT, ACCOUNT_INACTIVE, DAILY_LIMIT, QUEUE_FULL).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/{id}/request-changes": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "description": "Upload identifier.", "schema": { "type": "integer" } }
      ],
      "post": {
        "tags": ["Uploads"],
        "summary": "Request changes",
        "description": "Owner or admin only. Sends a post back to `draft`, with the note in `reviewComment`.",
        "operationId": "requestUploadChanges",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "comment": { "type": "string", "maxLength": 2000 } } } } } },
        "responses": {
          "200": { "description": "The post.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "uploadId": { "type": "integer" }, "status": { "$ref": "#/components/schemas/UploadStatus" }, "scheduledFor": { "type": ["string", "null"], "format": "date-time" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Only a team owner or admin can do this.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "The post is not in a state that allows it (`code`: NOT_HELD, NOT_DRAFT, ACCOUNT_INACTIVE, DAILY_LIMIT, QUEUE_FULL).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/upload/review-links": {
      "post": {
        "tags": ["Uploads"],
        "summary": "Create a client review link",
        "description": "Owner or admin only. A no-login link (14 days) where a client approves posts (which schedules them) or asks for changes with a note. Without `uploadIds`, every post awaiting approval, up to 50; drafts included are submitted.",
        "operationId": "createReviewLink",
        "x-required-scope": "posts:publish",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": { "required": false, "content": { "application/json": { "schema": { "type": "object", "properties": { "uploadIds": { "type": "array", "items": { "type": "integer" }, "maxItems": 50 } } } } } },
        "responses": {
          "200": { "description": "The link.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "url": { "type": "string", "format": "uri" }, "expiresAt": { "type": "string", "format": "date-time" }, "uploadIds": { "type": "array", "items": { "type": "integer" } } } } } } } } },
          "400": { "description": "Nothing to review, more than 50 posts, or unknown uploadIds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Only a team owner or admin can create one.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/x-credits": {
      "get": {
        "tags": ["Limits"],
        "summary": "Get X credit balance",
        "description": "X bills every post, so X posting is metered in credits: 1 per post, 15 when the text contains a link (detected with X's own twitter-text rules). Returns the balance (plan allowance left this month + purchased credits, which never expire), the cost per post and the packs on sale. An X post fails when the balance is short.",
        "operationId": "getXCredits",
        "x-required-scope": "posts:read",
        "responses": {
          "200": {
            "description": "Balance, prices and packs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "balance": {
                          "type": "object",
                          "properties": {
                            "monthlyAllowance": { "type": "integer", "description": "Credits included in the plan each UTC month." },
                            "includedRemaining": { "type": "integer" },
                            "purchased": { "type": "integer", "description": "Bought in packs; never expire." },
                            "total": { "type": "integer", "description": "includedRemaining + purchased." }
                          }
                        },
                        "costs": {
                          "type": "object",
                          "properties": {
                            "post": { "type": "integer", "example": 1 },
                            "linkPost": { "type": "integer", "example": 15 }
                          }
                        },
                        "packs": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": { "type": "string" },
                              "credits": { "type": "integer" },
                              "price": { "type": "number" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/limits/{accountId}": {
      "get": {
        "tags": ["Limits"],
        "summary": "Get daily publishing limits",
        "description": "Returns how many posts the account may still publish today and when the window resets. Limits are imposed by the social platform, not by Multi Upload Tool.",
        "operationId": "getAccountLimits",
        "x-required-scope": "posts:read",
        "parameters": [
          {
            "name": "accountId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The account's current limit state.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Limits" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "List webhooks",
        "description": "Returns every webhook registered by the caller, with the events each one subscribes to.",
        "operationId": "listWebhooks",
        "x-required-scope": "webhooks:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The caller's webhooks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Webhook" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Webhooks"],
        "summary": "Create a webhook",
        "description": "Registers an endpoint to receive the selected events. Team members with the `MEMBER` role cannot create webhooks.",
        "operationId": "createWebhook",
        "x-required-scope": "webhooks:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["url", "events"],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS endpoint that will receive the event payloads."
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": { "$ref": "#/components/schemas/WebhookEvent" }
                  },
                  "secret": {
                    "type": "string",
                    "description": "Shared secret used to sign deliveries."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created webhook.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Webhook" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{id}": {
      "delete": {
        "tags": ["Webhooks"],
        "summary": "Delete a webhook",
        "description": "Removes the webhook. Deliveries stop immediately; past deliveries are unaffected.",
        "operationId": "deleteWebhook",
        "x-required-scope": "webhooks:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Webhooks"],
        "summary": "Enable or disable a webhook",
        "description": "Re-enables a webhook disabled after 3 events in a row failed every delivery attempt (and resets that count), or pauses one without deleting it.",
        "operationId": "setWebhookActive",
        "x-required-scope": "webhooks:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["isActive"], "properties": { "isActive": { "type": "boolean" } } }
            }
          }
        },
        "responses": {
          "200": { "description": "The updated webhook.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "$ref": "#/components/schemas/Webhook" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{id}/deliveries": {
      "get": {
        "tags": ["Webhooks"],
        "summary": "List delivery attempts",
        "description": "Every delivery attempt, newest first, kept 30 days. One row per attempt: an event that failed twice then succeeded has 3 rows with the same `eventId`. A failed attempt is retried after 30 s, 2 min, 10 min, 1 h and 6 h.",
        "operationId": "listWebhookDeliveries",
        "x-required-scope": "webhooks:read",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Delivery attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{id}/deliveries/{deliveryId}/replay": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Replay a delivery",
        "description": "Sends the event of that delivery again, with its original event id so your endpoint can deduplicate. The replay gets its own attempts and retries. Returns 409 when the webhook is disabled.",
        "operationId": "replayWebhookDelivery",
        "x-required-scope": "webhooks:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" },
          { "name": "deliveryId", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "The event is queued again.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "eventId": { "type": "string" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "The webhook is disabled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{id}/rotate-secret": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Rotate the signing secret",
        "description": "Generates a new signing secret, effective immediately (retries already queued are signed with it too), and returns it.",
        "operationId": "rotateWebhookSecret",
        "x-required-scope": "webhooks:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "description": "The new secret.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "const": true }, "data": { "type": "object", "properties": { "secret": { "type": "string" } } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/webhooks/{id}/test": {
      "post": {
        "tags": ["Webhooks"],
        "summary": "Send a test delivery",
        "description": "Delivers a sample payload to the webhook so you can verify signing and connectivity.",
        "operationId": "testWebhook",
        "x-required-scope": "webhooks:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/teams": {
      "get": {
        "tags": ["Teams"],
        "summary": "Get the current team",
        "description": "Returns the team the token is scoped to, or the caller's own team.",
        "operationId": "getTeam",
        "x-required-scope": "team:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The team and the caller's role in it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Team" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/teams/members": {
      "get": {
        "tags": ["Teams"],
        "summary": "List team members",
        "description": "Returns the team's members with their role and whether their membership is active.",
        "operationId": "listTeamMembers",
        "x-required-scope": "team:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The team's members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/TeamMember" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/teams/members/{targetUserId}": {
      "parameters": [
        { "name": "targetUserId", "in": "path", "required": true, "schema": { "type": "integer" } },
        { "$ref": "#/components/parameters/TeamId" }
      ],
      "patch": {
        "tags": ["Teams"],
        "summary": "Update a member",
        "description": "Changes a member's role or activation state. Requires `OWNER` or `ADMIN`; only an owner may modify an admin.",
        "operationId": "updateTeamMember",
        "x-required-scope": "team:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": { "$ref": "#/components/schemas/TeamRole" },
                  "isActive": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Teams"],
        "summary": "Remove a member",
        "description": "Requires `OWNER` or `ADMIN`; only an owner may remove an admin.",
        "operationId": "removeTeamMember",
        "x-required-scope": "team:write",
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/teams/invites": {
      "post": {
        "tags": ["Teams"],
        "summary": "Invite a member",
        "description": "Sends a team invitation. Requires `OWNER` or `ADMIN`.",
        "operationId": "createTeamInvite",
        "x-required-scope": "team:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "role"],
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "role": { "type": "string", "enum": ["ADMIN", "MEMBER"] }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created invitation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "object", "additionalProperties": true }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/whitelabel/links": {
      "get": {
        "tags": ["White-label"],
        "summary": "List connection links",
        "description": "Returns the hosted connection links the caller has created, including ones already used or expired.",
        "operationId": "listWhitelabelLinks",
        "x-required-scope": "links:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The caller's hosted connection links.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/WhitelabelLink" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["White-label"],
        "summary": "Create a connection link",
        "description": "Creates a hosted, branded page where one of your own users can connect their social account to your workspace.",
        "operationId": "createWhitelabelLink",
        "x-required-scope": "links:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["platform"],
                "properties": {
                  "platform": { "$ref": "#/components/schemas/Platform" },
                  "expiresInHours": { "type": "integer", "description": "Lifetime of the link." },
                  "logoUrl": { "type": "string", "format": "uri" },
                  "title": { "type": "string" },
                  "description": { "type": "string" },
                  "redirectUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where to send the user once the account is connected."
                  },
                  "tags": { "type": "array", "items": { "type": "string" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created connection link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/WhitelabelLink" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/whitelabel/links/{id}": {
      "delete": {
        "tags": ["White-label"],
        "summary": "Delete a connection link",
        "description": "Invalidates the hosted link. Accounts already connected through it stay connected.",
        "operationId": "deleteWhitelabelLink",
        "x-required-scope": "links:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/short-links": {
      "get": {
        "tags": ["Short links"],
        "summary": "List short links",
        "description": "Returns the caller's short links with their click counts.",
        "operationId": "listShortLinks",
        "x-required-scope": "links:read",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          { "name": "page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 } },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": {
            "description": "The caller's short links.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/ShortLink" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Short links"],
        "summary": "Create a short link",
        "description": "Creates a trackable short link, optionally with routing rules that send different visitors to different destinations.",
        "operationId": "createShortLink",
        "x-required-scope": "links:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ShortLinkRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created short link, including its full URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/ShortLink" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/short-links/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
        { "$ref": "#/components/parameters/TeamId" }
      ],
      "get": {
        "tags": ["Short links"],
        "summary": "Get a short link",
        "description": "Returns one short link together with its routing rules.",
        "operationId": "getShortLink",
        "x-required-scope": "links:read",
        "responses": {
          "200": {
            "description": "The short link and its routing rules.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/ShortLink" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "put": {
        "tags": ["Short links"],
        "summary": "Update a short link",
        "description": "Replaces the link's details and routing rules.",
        "operationId": "updateShortLink",
        "x-required-scope": "links:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ShortLinkRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated short link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/ShortLink" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Short links"],
        "summary": "Delete a short link",
        "description": "Deletes the short link; its slug stops resolving.",
        "operationId": "deleteShortLink",
        "x-required-scope": "links:write",
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/domains": {
      "get": {
        "tags": ["Domains"],
        "summary": "List custom domains",
        "description": "Returns the custom short-link domains registered by the caller and whether each is verified.",
        "operationId": "listDomains",
        "x-required-scope": "links:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The caller's custom short-link domains.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Domain" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "tags": ["Domains"],
        "summary": "Add a custom domain",
        "description": "Registers a domain for short links. Point it at the platform with a CNAME, then call the verify endpoint.",
        "operationId": "createDomain",
        "x-required-scope": "links:write",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["domain"],
                "properties": {
                  "domain": { "type": "string", "examples": ["links.example.com"] },
                  "teamId": {
                    "type": "integer",
                    "description": "Team to attach the domain to. Must match the token's team when the token is team-scoped."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The registered domain and the DNS record to create.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Domain" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/domains/{id}": {
      "delete": {
        "tags": ["Domains"],
        "summary": "Delete a custom domain",
        "description": "Removes the custom domain from the account.",
        "operationId": "deleteDomain",
        "x-required-scope": "links:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/domains/{id}/verify": {
      "post": {
        "tags": ["Domains"],
        "summary": "Verify a custom domain",
        "description": "Checks that the domain's DNS points at the platform and activates it.",
        "operationId": "verifyDomain",
        "x-required-scope": "links:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "The verification result.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Domain" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/pinterest/boards": {
      "get": {
        "tags": ["Accounts"],
        "summary": "List Pinterest boards",
        "description": "Returns the boards a connected Pinterest account can pin to. Use a board id as `pinterest_board_id` when publishing.",
        "operationId": "listPinterestBoards",
        "x-required-scope": "accounts:read",
        "parameters": [
          { "$ref": "#/components/parameters/TeamId" },
          {
            "name": "accountId",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The account's boards.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/pools": {
      "get": {
        "tags": ["Pools"],
        "summary": "List pools",
        "description": "Pools hold media that is published to their attached accounts on a recurring schedule.",
        "operationId": "listPools",
        "x-required-scope": "pools:read",
        "parameters": [{ "$ref": "#/components/parameters/TeamId" }],
        "responses": {
          "200": {
            "description": "The caller's pools.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Pool" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/pools/{id}": {
      "get": {
        "tags": ["Pools"],
        "summary": "Get a pool",
        "description": "Returns one pool and its current state.",
        "operationId": "getPool",
        "x-required-scope": "pools:read",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "The pool.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Pool" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/pools/{id}/capacity": {
      "get": {
        "tags": ["Pools"],
        "summary": "Get remaining pool capacity",
        "description": "Returns how much more media the pool can accept before it hits its configured ceiling.",
        "operationId": "getPoolCapacity",
        "x-required-scope": "pools:read",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "The pool's capacity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "object", "additionalProperties": true }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/pools/{id}/videos": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
        { "$ref": "#/components/parameters/TeamId" }
      ],
      "get": {
        "tags": ["Pools"],
        "summary": "List media in a pool",
        "description": "Returns the media currently queued in the pool.",
        "operationId": "listPoolVideos",
        "x-required-scope": "pools:read",
        "responses": {
          "200": {
            "description": "The pool's queued media.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "tags": ["Pools"],
        "summary": "Add media to a pool",
        "description": "Adds one media file to the pool's queue.",
        "operationId": "addPoolVideo",
        "x-required-scope": "pools:write",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "video": { "type": "string", "format": "binary" },
                  "title": { "type": "string" },
                  "description": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "tags": ["Pools"],
        "summary": "Empty a pool",
        "description": "Removes every queued item from the pool.",
        "operationId": "clearPoolVideos",
        "x-required-scope": "pools:write",
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/pools/{id}/videos/bulk": {
      "post": {
        "tags": ["Pools"],
        "summary": "Add several media items to a pool",
        "description": "Adds several media files to the pool's queue in one request.",
        "operationId": "addPoolVideosBulk",
        "x-required-scope": "pools:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "videos": {
                    "type": "array",
                    "items": { "type": "string", "format": "binary" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/pools/{id}/videos/{itemId}": {
      "delete": {
        "tags": ["Pools"],
        "summary": "Remove one item from a pool",
        "description": "Removes one queued item without touching the rest of the pool.",
        "operationId": "deletePoolVideo",
        "x-required-scope": "pools:write",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "itemId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Success" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/analytics/overview": {
      "get": {
        "tags": ["Analytics"],
        "summary": "Global analytics across every connected account",
        "description": "Aggregates the collected metrics of all your active accounts.\n\nThe numbers come from Multi Upload Tool's own store, filled by a nightly collector — they are not read from the platforms at request time. Check `lastRefreshedAt` before presenting them as live: a number without its reading time looks real time, and the first time it disagrees with the platform's own app it reads as a bug rather than a collection that has not run yet.\n\nA metric a platform does not report is counted as `0`, not omitted: YouTube exposes no shares, Bluesky no views, Pinterest no likes.",
        "operationId": "getAnalyticsOverview",
        "x-required-scope": "analytics:read",
        "parameters": [
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Comma-separated account tags. Only accounts carrying one of them are aggregated.",
            "example": "client-a,client-b",
            "schema": { "type": "string" }
          },
          { "name": "source", "in": "query", "required": false, "description": "Narrow `videos` (the post ranking) to `mut` (published through Multi Upload Tool) or `external` (posted directly on the platform). `all` or absent keeps every post. Totals and `summary` always cover every post.", "schema": { "type": "string", "enum": ["all", "mut", "external"] } },
          { "name": "timezone", "in": "query", "required": false, "description": "IANA zone the `postingInsights` weekday and hour buckets are read in, e.g. `Europe/Paris`. Defaults to `UTC`. An hour-of-day ranking read in the wrong zone is silently off by the whole offset, so send the user's own.", "example": "Europe/Paris", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "Aggregated analytics for the matching accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Analytics" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/analytics/{connectedAccountId}": {
      "get": {
        "tags": ["Analytics"],
        "summary": "Analytics for one connected account",
        "description": "The same shape as the overview, restricted to a single account, plus that account's tags.",
        "operationId": "getAccountAnalytics",
        "x-required-scope": "analytics:read",
        "parameters": [
          { "name": "connectedAccountId", "in": "path", "required": true, "description": "Id of the connected account, as returned by `GET /accounts`.", "schema": { "type": "string" } },
          { "name": "source", "in": "query", "required": false, "description": "Narrow `videos` (the post ranking) to `mut` (published through Multi Upload Tool) or `external` (posted directly on the platform). `all` or absent keeps every post. Totals and `summary` always cover every post.", "schema": { "type": "string", "enum": ["all", "mut", "external"] } },
          { "name": "timezone", "in": "query", "required": false, "description": "IANA zone the `postingInsights` weekday and hour buckets are read in, e.g. `Europe/Paris`. Defaults to `UTC`. An hour-of-day ranking read in the wrong zone is silently off by the whole offset, so send the user's own.", "example": "Europe/Paris", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "Analytics for the account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Analytics" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/analytics/{connectedAccountId}/sync": {
      "post": {
        "tags": ["Analytics"],
        "summary": "Refresh one account now instead of waiting for the nightly pass",
        "description": "Reads the platform inside the request, writes the snapshots, then returns the refreshed analytics. Covers up to the 1000 most recent posts.\n\nThis is a write: it rotates OAuth tokens, stores snapshots and consumes the platform's rate limit, so it needs manage access to the account, not read access. Use it when someone has just published from the platform's own app — the collector otherwise runs once a night.",
        "operationId": "syncAccountAnalytics",
        "x-required-scope": "analytics:read",
        "parameters": [
          { "name": "connectedAccountId", "in": "path", "required": true, "description": "Id of the connected account, as returned by `GET /accounts`.", "schema": { "type": "string" } },
          { "name": "timezone", "in": "query", "required": false, "description": "IANA zone the `postingInsights` weekday and hour buckets are read in, e.g. `Europe/Paris`. Defaults to `UTC`. An hour-of-day ranking read in the wrong zone is silently off by the whole offset, so send the user's own.", "example": "Europe/Paris", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "Analytics as of the sync that just ran.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": { "$ref": "#/components/schemas/Analytics" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/analytics/{connectedAccountId}/linkedin/comments": {
      "get": {
        "tags": ["Analytics"],
        "summary": "List the commenters on a LinkedIn post",
        "description": "LinkedIn only — any other platform returns `400`. Read-only: there is no endpoint to reply to a comment.",
        "operationId": "getLinkedInPostComments",
        "x-required-scope": "analytics:read",
        "parameters": [
          { "name": "connectedAccountId", "in": "path", "required": true, "description": "Id of the connected account, as returned by `GET /accounts`.", "schema": { "type": "string" } },
          { "name": "postId", "in": "query", "required": true, "description": "The LinkedIn post URN, as returned in the upload result.", "example": "urn:li:share:7212345678901234567", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/TeamId" }
        ],
        "responses": {
          "200": {
            "description": "The comments on the post.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "const": true },
                    "data": {
                      "type": "object",
                      "properties": {
                        "comments": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationError" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "API token created from the dashboard, in the form `kmb_<64 hex characters>`.\n\nA token carries a list of scopes, chosen when it is created: `*` (everything — every token created before scopes existed, and any token created without a list) or `resource:action` scopes such as `posts:read` or `analytics:read`. Each operation states the scope it needs in `x-required-scope`; a token without it gets `403` with `code: INSUFFICIENT_SCOPE`. A token with `posts:write` but not `posts:publish` can create posts, which are always held for approval. On top of scopes, team scoping applies: baked into the token at creation time or requested per-call with `X-Team-ID`. Store it as you would a password."
      }
    },
    "parameters": {
      "TeamId": {
        "name": "X-Team-ID",
        "in": "header",
        "required": false,
        "description": "Act inside this team. Ignored when the token is already pinned to a team; membership is verified on every request.",
        "schema": { "type": "integer" }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Unique value (e.g. a UUID) per intended post. A retry with the same key within 24 hours returns the first response, with the `Idempotent-Replay: true` header, instead of publishing again.",
        "schema": { "type": "string", "maxLength": 255 }
      }
    },
    "responses": {
      "Success": {
        "description": "The operation succeeded.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "success": { "const": true },
                "message": { "type": "string" }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The API key is missing, invalid, revoked or expired.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "The token is valid but not permitted to act on this resource, usually because of team scoping or the caller's team role.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No such resource, or it does not belong to the caller.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ValidationError": {
        "description": "The request body or parameters were rejected.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                { "$ref": "#/components/schemas/Error" },
                { "$ref": "#/components/schemas/FieldValidationError" }
              ]
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests for this endpoint group.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Platform": {
        "type": "string",
        "description": "A supported social platform.",
        "enum": [
          "tiktok",
          "youtube",
          "instagram",
          "facebook",
          "linkedin",
          "pinterest",
          "bluesky",
          "threads",
          "x"
        ]
      },
      "AccountStatus": {
        "type": "string",
        "description": "Connection state of a social account. Only `active` accounts can publish.",
        "enum": ["active", "expired", "revoked", "inactive"]
      },
      "UploadStatus": {
        "type": "string",
        "enum": ["pending", "scheduled", "processing", "completed", "failed", "draft", "pending_approval"]
      },
      "TeamRole": {
        "type": "string",
        "enum": ["OWNER", "ADMIN", "MEMBER"]
      },
      "Error": {
        "type": "object",
        "required": ["success", "error"],
        "properties": {
          "success": { "const": false },
          "error": { "type": "string", "description": "Human-readable failure reason." },
          "statusCode": { "type": "integer" }
        }
      },
      "FieldValidationError": {
        "type": "object",
        "required": ["success", "message", "errors"],
        "properties": {
          "success": { "const": false },
          "message": { "const": "Validation Error" },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "path": { "type": "string" },
                "message": { "type": "string" },
                "summary": { "type": "string" }
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "platform": { "$ref": "#/components/schemas/Platform" },
          "accountName": { "type": "string", "description": "Display name or handle on the platform." },
          "profileImageUrl": { "type": ["string", "null"], "format": "uri" },
          "status": { "$ref": "#/components/schemas/AccountStatus" },
          "tags": { "type": "array", "items": { "type": "string" } }
        }
      },
      "Upload": {
        "type": "object",
        "description": "One publication to one account.",
        "properties": {
          "id": { "type": "integer" },
          "platform": { "$ref": "#/components/schemas/Platform" },
          "accountId": { "type": "string" },
          "title": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/UploadStatus" },
          "url": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "Public URL of the published post, once it succeeds."
          },
          "error": { "type": ["string", "null"], "description": "Failure reason when status is `failed`." },
          "failure": { "$ref": "#/components/schemas/UploadFailure" },
          "xCredits": {
            "type": ["object", "null"],
            "description": "X posts only: credits this post cost. `spent` is what was debited (1, or 15 when the text contains a link), `refunded` what came back when the post failed, `net` the difference. Null until the job has run.",
            "properties": {
              "spent": { "type": "integer" },
              "refunded": { "type": "integer" },
              "net": { "type": "integer" }
            }
          },
          "scheduledFor": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "UploadFailure": {
        "type": ["object", "null"],
        "description": "What a failure means, as data. Null unless status is `failed`.",
        "required": ["code", "retryable", "nextAction"],
        "properties": {
          "code": {
            "type": "string",
            "enum": ["rate_limited", "account_disconnected", "platform_unavailable", "queue_expired", "unknown", "media_invalid", "media_unreachable", "content_flagged", "platform_processing_timeout", "cancelled"]
          },
          "retryable": { "type": "boolean", "description": "Whether retrying the same post can succeed." },
          "nextAction": {
            "type": "string",
            "enum": ["retry", "retry_later", "reconnect_account", "replace_media", "edit_post", "check_platform", "none"]
          }
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "total": { "type": "integer", "description": "Total matching records, across all pages." },
          "page": { "type": "integer" },
          "limit": { "type": "integer" },
          "totalPages": { "type": "integer" }
        }
      },
      "QueuedUpload": {
        "type": "object",
        "properties": {
          "uploadId": { "type": "integer" },
          "status": { "$ref": "#/components/schemas/UploadStatus" },
          "scheduledFor": { "type": ["string", "null"], "format": "date-time" },
          "jobId": { "type": ["string", "null"], "description": "Background job handle." }
        }
      },
      "Limits": {
        "type": "object",
        "properties": {
          "success": { "const": true },
          "allowed": { "type": "boolean", "description": "Whether another post may be published right now." },
          "used": { "type": "integer" },
          "limit": { "type": "integer" },
          "remaining": { "type": "integer" },
          "resetAt": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookEvent": {
        "type": "string",
        "enum": [
          "upload.completed",
          "upload.failed",
          "upload.scheduled",
          "upload.cancelled",
          "upload.approved",
          "upload.changes_requested",
          "connected_account.expired",
          "connected_account.refresh_failed",
          "connected_account.connected",
          "connected_account.disconnected",
          "subscription.created",
          "subscription.updated",
          "subscription.cancelled",
          "subscription.expired",
          "subscription.revoked",
          "upsell.purchased",
          "upsell.subscription.created",
          "upsell.subscription.revoked",
          "suggestion.created",
          "suggestion.updated",
          "security.login",
          "team.invite",
          "webhook.disabled"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "One delivery attempt.",
        "properties": {
          "id": { "type": "integer" },
          "eventId": { "type": "string", "description": "The payload id, identical across retries and replays." },
          "event": { "$ref": "#/components/schemas/WebhookEvent" },
          "attempt": { "type": "integer", "description": "1 to 6 within one delivery." },
          "success": { "type": "boolean" },
          "statusCode": { "type": ["integer", "null"] },
          "error": { "type": ["string", "null"] },
          "durationMs": { "type": ["integer", "null"] },
          "payload": { "type": "object" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "url": { "type": "string", "format": "uri" },
          "events": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookEvent" } },
          "isActive": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Team": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "role": { "$ref": "#/components/schemas/TeamRole" }
        }
      },
      "TeamMember": {
        "type": "object",
        "properties": {
          "userId": { "type": "integer" },
          "email": { "type": "string", "format": "email" },
          "role": { "$ref": "#/components/schemas/TeamRole" },
          "isActive": { "type": "boolean" }
        }
      },
      "WhitelabelLink": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "platform": { "$ref": "#/components/schemas/Platform" },
          "url": { "type": "string", "format": "uri", "description": "Hosted page to send your user to." },
          "title": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "logoUrl": { "type": ["string", "null"], "format": "uri" },
          "redirectUrl": { "type": ["string", "null"], "format": "uri" },
          "tags": { "type": "array", "items": { "type": "string" } },
          "expiresAt": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "ShortLinkRule": {
        "type": "object",
        "description": "Sends a subset of visitors to a different destination. Rules are evaluated by descending priority.",
        "required": ["type", "destination"],
        "properties": {
          "type": { "type": "string", "enum": ["geo", "device", "language", "time", "referer"] },
          "destination": { "type": "string", "format": "uri" },
          "countryCodes": { "type": "string", "maxLength": 500, "description": "Comma-separated ISO 3166-1 alpha-2 codes." },
          "languages": { "type": "string", "maxLength": 500 },
          "devices": { "type": "string", "maxLength": 500 },
          "referers": { "type": "string", "maxLength": 1000 },
          "startTime": { "type": "string" },
          "endTime": { "type": "string" },
          "days": { "type": "string" },
          "timezone": { "type": "string", "maxLength": 50 },
          "priority": { "type": "integer", "minimum": 0, "maximum": 1000 },
          "useDeeplink": { "type": "boolean" },
          "isActive": { "type": "boolean" }
        }
      },
      "ShortLinkRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Default destination." },
          "slug": {
            "type": "string",
            "minLength": 1,
            "maxLength": 150,
            "pattern": "^[a-zA-Z0-9-_.]+$",
            "description": "Custom path. Generated when omitted."
          },
          "domainId": { "type": "integer", "description": "Custom domain to serve the link from." },
          "title": { "type": "string", "maxLength": 200 },
          "description": { "type": "string", "maxLength": 1000 },
          "imageUrl": { "type": "string", "format": "uri", "maxLength": 2048 },
          "useDeeplink": { "type": "boolean", "description": "Open the destination in the target app rather than a browser." },
          "botProtection": { "type": "boolean", "description": "Send detected bots to `safeUrl` instead." },
          "safeUrl": { "type": "string", "format": "uri", "maxLength": 2048 },
          "rules": {
            "type": "array",
            "maxItems": 50,
            "items": { "$ref": "#/components/schemas/ShortLinkRule" }
          }
        }
      },
      "ShortLink": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "shortUrl": { "type": "string", "format": "uri", "description": "The full short URL, on the default or custom domain." },
          "title": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "imageUrl": { "type": ["string", "null"], "format": "uri" },
          "domainId": { "type": ["integer", "null"] },
          "useDeeplink": { "type": "boolean" },
          "botProtection": { "type": "boolean" },
          "safeUrl": { "type": ["string", "null"], "format": "uri" },
          "clicks": { "type": "integer" },
          "rules": { "type": "array", "items": { "$ref": "#/components/schemas/ShortLinkRule" } },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Domain": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "domain": { "type": "string" },
          "verified": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      },
      "Pool": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "isActive": { "type": "boolean" },
          "itemCount": { "type": "integer" },
          "staggerMinutes": { "type": "integer", "minimum": 0, "maximum": 60, "description": "Each slot's accounts go out at random moments spread over this many minutes instead of all at the slot time. 0 = off. Set in the app; always shorter than the gap to the next slot." }
        }
      },
      "PresignRoute": {
        "type": "string",
        "description": "Validation preset applied to the declared files: it fixes the accepted MIME types, the maximum number of files, the maximum size per file, and whether the upload is multipart. It does not tie the media to a platform.",
        "enum": [
          "youtube_videos",
          "tiktok_videos",
          "pinterest_videos",
          "threads_videos",
          "x_videos",
          "linkedin_videos",
          "documents",
          "photos",
          "thumbnails",
          "instagram_photos",
          "threads_photos",
          "x_photos",
          "bluesky_photos"
        ]
      },
      "PresignFile": {
        "type": "object",
        "description": "One file to presign. The declared size is signed into the URLs, so it must be the file's exact byte length.",
        "required": ["name", "size", "type"],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Original file name. Sanitised into the stored object's key; the extension is kept and is what identifies the media type downstream.",
            "examples": ["clip.mp4"]
          },
          "size": {
            "type": "integer",
            "minimum": 1,
            "description": "Exact byte length. Zero is rejected, and a value that disagrees with what you upload fails the signed request.",
            "examples": [734003200]
          },
          "type": {
            "type": "string",
            "description": "MIME type, checked against the route's accepted types.",
            "examples": ["video/mp4"]
          }
        }
      },
      "PresignRequest": {
        "type": "object",
        "required": ["route", "files"],
        "properties": {
          "route": { "$ref": "#/components/schemas/PresignRoute" },
          "files": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/PresignFile" }
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Accepted and ignored, so bodies produced by generic presigned-upload clients validate."
          }
        }
      },
      "PresignPart": {
        "type": "object",
        "description": "One part of a multipart upload.",
        "properties": {
          "partNumber": { "type": "integer", "description": "1-based. Part `n` covers the bytes starting at `(n - 1) * partSize`." },
          "size": { "type": "integer", "description": "Bytes this part must carry, sent as its `Content-Length`. The last part is shorter than `partSize`." },
          "uploadUrl": { "type": "string", "format": "uri", "description": "`PUT` the part's bytes here." }
        }
      },
      "PresignMultipart": {
        "type": "object",
        "description": "Present on multipart routes. Upload every part, then complete; abort if you give up.",
        "properties": {
          "uploadId": { "type": "string", "description": "Storage-side identifier of the multipart upload." },
          "partSize": { "type": "integer", "description": "Bytes per part, except the last one." },
          "completeUrl": {
            "type": "string",
            "format": "uri",
            "description": "`POST` the `CompleteMultipartUpload` XML here, `Content-Type: application/xml`, parts sorted ascending. Valid 6 hours."
          },
          "abortUrl": {
            "type": "string",
            "format": "uri",
            "description": "`DELETE` here to discard an abandoned upload. Parts left behind are stored and billed."
          },
          "parts": { "type": "array", "items": { "$ref": "#/components/schemas/PresignPart" } }
        }
      },
      "PresignedFile": {
        "type": "object",
        "description": "Where to send one file's bytes, and the URLs to publish it with afterwards.",
        "properties": {
          "name": { "type": "string" },
          "size": { "type": "integer" },
          "contentType": { "type": "string" },
          "key": { "type": "string", "description": "Object key the media is stored under." },
          "bucketUrl": {
            "type": "string",
            "format": "uri",
            "description": "Preferred for large video: pass it as the `video` field of `POST /upload` or `POST /upload/bulk` and the media is read straight from storage, instead of streaming every gigabyte back through this API. Points at private storage, so it is not fetchable on its own - only this API can read it."
          },
          "accessUrl": {
            "type": "string",
            "format": "uri",
            "description": "The publicly fetchable form of the same object: pass it as the `video`, `photo` or `file` field of `POST /upload` or `POST /upload/bulk`. Served from a platform-verified domain, so pull-based platforms can fetch it. Readable once the bytes are stored. Pass it through unchanged; a rehosted copy on your own domain is rejected by those platforms."
          },
          "uploadUrl": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "Single-request routes only: `PUT` the whole file here, with `uploadHeaders`. Valid 1 hour. `null` on multipart routes."
          },
          "uploadHeaders": {
            "type": ["object", "null"],
            "additionalProperties": { "type": "string" },
            "description": "Headers to send verbatim with the single `PUT`. They are part of the signature. `null` on multipart routes."
          },
          "multipart": {
            "description": "Multipart routes only; `null` on single-request routes.",
            "anyOf": [{ "$ref": "#/components/schemas/PresignMultipart" }, { "type": "null" }]
          }
        }
      },
      "StorageUsage": {
        "type": "object",
        "description": "Presigned bytes charged against the caller's rolling allowance.",
        "properties": {
          "windowDays": { "type": "integer", "description": "Length of the rolling window, in days." },
          "used": { "type": "integer", "description": "Bytes presigned inside the window, including the current request." },
          "limit": { "type": "integer", "description": "Allowance in bytes for the window. `-1` when the plan has no allowance configured." },
          "remaining": { "type": "integer", "description": "Bytes still available. `-1` when unlimited." },
          "unlimited": { "type": "boolean", "description": "When true the quota never blocks the request." }
        }
      },
      "PresignRouteLimits": {
        "type": "object",
        "description": "The limits of the route that rejected the request.",
        "properties": {
          "route": { "$ref": "#/components/schemas/PresignRoute" },
          "maxFileSize": { "type": "integer", "description": "Largest accepted size per file, in bytes." },
          "maxFiles": { "type": "integer" },
          "fileTypes": { "type": "array", "items": { "type": "string" } }
        }
      },
      "PresignResponse": {
        "type": "object",
        "required": ["success", "data"],
        "properties": {
          "success": { "const": true },
          "data": {
            "type": "object",
            "required": ["route", "files"],
            "properties": {
              "route": { "$ref": "#/components/schemas/PresignRoute" },
              "partSize": {
                "type": ["integer", "null"],
                "description": "Bytes per part on multipart routes, `null` on single-request routes. Repeated inside each file's `multipart` block."
              },
              "expiresAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the returned upload URLs stop being accepted: one hour out for single-request routes, six for multipart ones. There is no refresh; presign again."
              },
              "usage": { "$ref": "#/components/schemas/StorageUsage" },
              "files": { "type": "array", "items": { "$ref": "#/components/schemas/PresignedFile" } }
            }
          }
        }
      },
      "PresignError": {
        "type": "object",
        "description": "The standard error envelope plus a machine-readable `code`, and whichever context block explains the failure.",
        "required": ["success", "error", "code"],
        "properties": {
          "success": { "const": false },
          "error": { "type": "string", "description": "Human-readable failure reason." },
          "code": {
            "type": "string",
            "enum": [
              "INVALID_JSON",
              "INVALID_REQUEST",
              "UNKNOWN_ROUTE",
              "TOO_MANY_FILES",
              "FILE_TOO_LARGE",
              "INVALID_FILE_TYPE",
              "REJECTED",
              "STORAGE_QUOTA_EXCEEDED"
            ]
          },
          "validRoutes": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PresignRoute" },
            "description": "Accepted route names. Returned with `UNKNOWN_ROUTE`."
          },
          "limits": { "$ref": "#/components/schemas/PresignRouteLimits" },
          "usage": { "$ref": "#/components/schemas/StorageUsage" }
        }
      },
      "UploadRequest": {
        "type": "object",
        "required": ["accountId"],
        "description": "Media may be supplied as a file part or as a publicly reachable URL string in the same field. Fields belonging to other platforms are ignored, except an unknown `x_*` field, refused (400) with `x_<field>: <message>`. X: the post fails (X credits refunded) when the same text (case, spaces, punctuation and emoji ignored; captions of 20+ letters or digits) or the exact same media file (SHA-256) was posted on another X account of the same owner in the last 30 days, because X's developer policy bans identical content across accounts.",
        "properties": {
          "accountId": {
            "description": "Target connected account.",
            "oneOf": [{ "type": "string" }, { "type": "integer" }]
          },
          "video": { "$ref": "#/components/schemas/MediaField" },
          "photo": { "$ref": "#/components/schemas/MediaField" },
          "file": { "$ref": "#/components/schemas/MediaField" },
          "title": { "type": "string" },
          "description": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["draft", "pending_approval"],
            "description": "Create without publishing: a draft, or a post awaiting approval. It is only published once approved (POST /upload/{id}/approve or a client review link). In a team that requires approval, a MEMBER's posts are always `pending_approval`."
          },
          "schedule_date": {
            "type": "string",
            "format": "date-time",
            "description": "Publish at this time instead of immediately (TikTok, Instagram, Facebook, LinkedIn, Pinterest, Bluesky, Threads)."
          },
          "link": { "type": "string", "format": "uri", "description": "Facebook link posts." },

          "privacy_level": { "type": "string", "description": "TikTok. E.g. `PUBLIC_TO_EVERYONE`, `SELF_ONLY`, `MUTUAL_FOLLOW_FRIENDS`." },
          "disable_comment": { "type": "boolean", "description": "TikTok." },
          "disable_duet": { "type": "boolean", "description": "TikTok." },
          "disable_stitch": { "type": "boolean", "description": "TikTok." },
          "brand_content_toggle": { "type": "boolean", "description": "TikTok: paid partnership disclosure." },
          "brand_organic_toggle": { "type": "boolean", "description": "TikTok: own-brand disclosure." },
          "is_aigc": { "type": "boolean", "description": "TikTok: content is AI-generated." },
          "post_mode": { "type": "string", "enum": ["DIRECT_POST", "MEDIA_UPLOAD"], "description": "TikTok. `DIRECT_POST` (default) publishes. `MEDIA_UPLOAD` sends the post to the creator's TikTok inbox instead, where they finish and publish it in the app; TikTok allows at most 5 pending inbox posts per 24 hours." },
          "media_type": { "type": "string", "description": "TikTok: video or photo carousel. Instagram: `REELS`, `IMAGE`, `CAROUSEL` or `STORIES`, auto-detected when omitted.\n\n`STORIES` publishes to the account's story instead of the feed. It takes exactly one media — a JPEG image up to 8 MB (PNG is refused), or an MP4/MOV video of 3 to 60 seconds up to 100 MB — and nothing else: `collaborators` and `location_id` are refused, the caption is dropped because stories have none, and the AI label is not sent because the API has no such field on stories. A story expires after 24 hours, has no permanent URL and no analytics." },
          "video_cover_timestamp_ms": { "type": "integer", "description": "TikTok: cover frame offset." },
          "auto_add_music": { "type": "boolean", "description": "TikTok photo posts." },
          "photo_cover_index": { "type": "integer", "description": "TikTok photo posts." },

          "youtube_title": { "type": "string", "description": "YouTube: overrides `title`." },
          "youtube_description": { "type": "string", "description": "YouTube: overrides `description`." },
          "tags": {
            "description": "YouTube tags. Comma-separated string or array. The platform caps the serialised list at 500 characters.",
            "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }]
          },
          "categoryId": { "type": "string", "description": "YouTube category id." },
          "privacyStatus": { "type": "string", "description": "YouTube. `public`, `private` or `unlisted`." },
          "thumbnail": { "type": "string", "format": "binary", "description": "YouTube custom thumbnail." },
          "thumbnail_url": { "type": "string", "format": "uri", "description": "YouTube custom thumbnail by URL." },
          "scheduledDate": { "type": "string", "format": "date-time", "description": "YouTube publication time." },
          "embeddable": { "type": "boolean", "description": "YouTube." },
          "license": { "type": "string", "description": "YouTube." },
          "publicStatsViewable": { "type": "boolean", "description": "YouTube." },
          "madeForKids": { "type": "boolean", "description": "YouTube." },
          "selfDeclaredMadeForKids": { "type": "boolean", "description": "YouTube." },
          "containsSyntheticMedia": { "type": "boolean", "description": "YouTube: altered or synthetic content disclosure." },
          "defaultLanguage": { "type": "string", "description": "YouTube." },
          "defaultAudioLanguage": { "type": "string", "description": "YouTube." },
          "allowedCountries": { "type": "string", "description": "YouTube: comma-separated country allowlist." },
          "blockedCountries": { "type": "string", "description": "YouTube: comma-separated country blocklist." },
          "hasPaidProductPlacement": { "type": "boolean", "description": "YouTube." },
          "recordingDate": { "type": "string", "format": "date-time", "description": "YouTube." },
          "notifySubscribers": { "type": "boolean", "description": "YouTube." },

          "caption": { "type": "string", "description": "Instagram: overrides `description`." },
          "cover_url": { "type": "string", "format": "uri", "description": "Instagram Reels cover." },
          "collaborators": { "type": "string", "description": "Instagram: comma-separated usernames." },
          "user_tags": { "type": "string", "description": "Instagram." },
          "location_id": { "type": "string", "description": "Instagram. Not accepted on a story." },
          "share_to_feed": { "type": "boolean", "description": "Instagram Reels: also show the Reel in the feed. Default `true`." },
          "trial_reel": { "type": "string", "enum": ["manual", "auto"], "description": "Instagram Trial Reel: shown to non-followers first. `manual` = the creator graduates it to followers in the Instagram app, `auto` = Instagram graduates it if it performs well. Single-video Reels only: a photo, carousel or story with `trial_reel` is refused (400) rather than published to followers. The account must be eligible for Trial Reels." },
          "audio_name": { "type": "string", "description": "Instagram Reels: display title of the Reel's original audio." },
          "thumb_offset": { "type": "string", "description": "Instagram Reels: cover frame offset, in ms." },

          "threads_reply_control": { "type": "string", "enum": ["everyone", "accounts_you_follow", "mentioned_only", "parent_post_author_only", "followers_only"], "description": "Threads: who can reply. Default `everyone`." },
          "threads_alt_text": { "type": "string", "maxLength": 1000, "description": "Threads: image alt text." },

          "x_reply_settings": { "type": "string", "enum": ["everyone", "following", "mentionedUsers", "subscribers", "verified"], "description": "X: who can reply. Default `everyone`." },
          "x_made_with_ai": { "type": "boolean", "description": "X: label the post as made with AI." },
          "x_paid_partnership": { "type": "boolean", "description": "X: label the post as a paid partnership." },
          "x_alt_text": { "type": "string", "maxLength": 1000, "description": "X: alt text applied to every image. Images only." },
          "x_tagged_users": { "type": "array", "maxItems": 10, "items": { "type": "string", "pattern": "^@?\\w{1,15}$" }, "description": "X: usernames tagged in the photos (multipart: JSON array or comma-separated). Images only. A tag only shows if the person allows photo tagging." },
          "x_subtitles_url": { "type": "string", "format": "uri", "description": "X: SRT or VTT file (max 1 MB) shown as captions. Video only." },
          "x_subtitles_language": { "type": "string", "pattern": "^[a-zA-Z]{2}$", "description": "X: 2-letter language code of the subtitles. Default `EN`. Needs `x_subtitles_url`." },
          "x_poll_options": { "type": "array", "minItems": 2, "maxItems": 4, "items": { "type": "string", "minLength": 1, "maxLength": 25 }, "description": "X: poll choices (multipart: JSON array or comma-separated). Text-only posts: X refuses a poll with media." },
          "x_poll_duration_minutes": { "type": "integer", "minimum": 5, "maximum": 10080, "description": "X: poll duration. Default `1440`. Needs `x_poll_options`." },
          "x_community_id": { "type": "string", "pattern": "^[0-9]{1,19}$", "description": "X: post to this Community." },
          "x_share_with_followers": { "type": "boolean", "description": "X: with `x_community_id`, also show the post to followers." },
          "x_super_followers_only": { "type": "boolean", "description": "X: only Super Followers see the post. The account needs X subscriptions enabled." },
          "x_premium": { "type": "boolean", "description": "X: the account has X Premium, so text up to 25,000 characters instead of 280 and videos up to 125 minutes instead of 20." },

          "visibility": { "type": "string", "enum": ["PUBLIC", "CONNECTIONS"], "description": "LinkedIn." },

          "bluesky_langs": { "type": "string", "description": "Bluesky: comma-separated language codes (e.g. `en,fr`) or a JSON array." },
          "bluesky_labels": { "type": "string", "description": "Bluesky content warnings: `sexual`, `nudity`, `porn`, `graphic-media`, `!no-unauthenticated`." },

          "pinterest_board_id": { "type": "string", "description": "Pinterest: board to pin to." },
          "board_id": { "type": "string", "description": "Pinterest: alias of `pinterest_board_id`." },
          "alt_text": { "type": "string", "description": "Pinterest: image alt text. On `POST /upload`, also used for Threads when `threads_alt_text` is absent." }
        }
      },
      "BulkUploadRequest": {
        "type": "object",
        "required": ["accounts"],
        "description": "Same options as `UploadRequest`, fanned out across several accounts. At most one X account per batch (X bans identical content across accounts): the extra X accounts get a per-account error. Each platform option only applies to the accounts of that platform, except `media_type`, which applies to every platform of the batch that reads it. `video` must be a file part here (no URL string).",
        "properties": {
          "accounts": {
            "description": "Target connected accounts, as an array or a JSON-encoded array string.",
            "oneOf": [
              { "type": "array", "items": { "type": "string" } },
              { "type": "string" }
            ]
          },
          "video": { "type": "string", "format": "binary" },
          "photo": { "$ref": "#/components/schemas/MediaField" },
          "title": { "type": "string" },
          "description": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["draft", "pending_approval"],
            "description": "Create without publishing: a draft, or a post awaiting approval. It is only published once approved (POST /upload/{id}/approve or a client review link). In a team that requires approval, a MEMBER's posts are always `pending_approval`."
          },
          "schedule_date": { "type": "string", "format": "date-time" },
          "overrides": {
            "type": "string",
            "description": "JSON object of per-network or per-account variants: keys are a platform name or an account id of this post, values `{ caption?, title?, thumbnail_url? }`. An account key wins over its platform key, which wins over `description` / `title`, field by field. Each account is validated against its own text. An unknown key returns 400.",
            "example": "{\"bluesky\": {\"caption\": \"Short version\"}}"
          },
          "link": { "type": "string", "format": "uri", "description": "Facebook link posts." },

          "privacy_level": { "type": "string", "description": "TikTok. E.g. `PUBLIC_TO_EVERYONE`, `SELF_ONLY`, `MUTUAL_FOLLOW_FRIENDS`." },
          "disable_comment": { "type": "boolean", "description": "TikTok." },
          "disable_duet": { "type": "boolean", "description": "TikTok." },
          "disable_stitch": { "type": "boolean", "description": "TikTok." },
          "brand_content_toggle": { "type": "boolean", "description": "TikTok: paid partnership disclosure." },
          "brand_organic_toggle": { "type": "boolean", "description": "TikTok: own-brand disclosure." },
          "is_aigc": { "type": "boolean", "description": "TikTok: content is AI-generated." },
          "post_mode": { "type": "string", "enum": ["DIRECT_POST", "MEDIA_UPLOAD"], "description": "TikTok. `DIRECT_POST` (default) publishes. `MEDIA_UPLOAD` sends the post to the creator's TikTok inbox instead, where they finish and publish it in the app; TikTok allows at most 5 pending inbox posts per 24 hours." },
          "media_type": { "type": "string", "description": "TikTok: video or photo carousel. Instagram: `REELS`, `IMAGE`, `CAROUSEL` or `STORIES`, auto-detected when omitted.\n\n`STORIES` publishes to the account's story instead of the feed. It takes exactly one media — a JPEG image up to 8 MB (PNG is refused), or an MP4/MOV video of 3 to 60 seconds up to 100 MB — and nothing else: `collaborators` and `location_id` are refused, the caption is dropped because stories have none, and the AI label is not sent because the API has no such field on stories. A story expires after 24 hours, has no permanent URL and no analytics." },
          "video_cover_timestamp_ms": { "type": "integer", "description": "TikTok: cover frame offset." },
          "auto_add_music": { "type": "boolean", "description": "TikTok photo posts." },
          "photo_cover_index": { "type": "integer", "description": "TikTok photo posts." },

          "youtube_title": { "type": "string", "description": "YouTube: overrides `title`." },
          "youtube_description": { "type": "string", "description": "YouTube: overrides `description`." },
          "tags": {
            "description": "YouTube tags. Comma-separated string or array. The platform caps the serialised list at 500 characters.",
            "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }]
          },
          "categoryId": { "type": "string", "description": "YouTube category id." },
          "privacyStatus": { "type": "string", "description": "YouTube. `public`, `private` or `unlisted`." },
          "thumbnail": { "type": "string", "format": "binary", "description": "YouTube custom thumbnail." },
          "thumbnail_url": { "type": "string", "format": "uri", "description": "YouTube custom thumbnail by URL." },
          "scheduledDate": { "type": "string", "format": "date-time", "description": "YouTube publication time." },
          "embeddable": { "type": "boolean", "description": "YouTube." },
          "license": { "type": "string", "description": "YouTube." },
          "publicStatsViewable": { "type": "boolean", "description": "YouTube." },
          "madeForKids": { "type": "boolean", "description": "YouTube." },
          "selfDeclaredMadeForKids": { "type": "boolean", "description": "YouTube." },
          "containsSyntheticMedia": { "type": "boolean", "description": "YouTube: altered or synthetic content disclosure." },
          "defaultLanguage": { "type": "string", "description": "YouTube." },
          "defaultAudioLanguage": { "type": "string", "description": "YouTube." },
          "allowedCountries": { "type": "string", "description": "YouTube: comma-separated country allowlist." },
          "blockedCountries": { "type": "string", "description": "YouTube: comma-separated country blocklist." },
          "hasPaidProductPlacement": { "type": "boolean", "description": "YouTube." },
          "recordingDate": { "type": "string", "format": "date-time", "description": "YouTube." },
          "notifySubscribers": { "type": "boolean", "description": "YouTube." },

          "caption": { "type": "string", "description": "Instagram: overrides `description`." },
          "cover_url": { "type": "string", "format": "uri", "description": "Instagram Reels cover." },
          "collaborators": { "type": "string", "description": "Instagram: comma-separated usernames." },
          "user_tags": { "type": "string", "description": "Instagram." },
          "location_id": { "type": "string", "description": "Instagram. Not accepted on a story." },
          "share_to_feed": { "type": "boolean", "description": "Instagram Reels: also show the Reel in the feed. Default `true`." },
          "trial_reel": { "type": "string", "enum": ["manual", "auto"], "description": "Instagram Trial Reel: shown to non-followers first. `manual` = the creator graduates it to followers in the Instagram app, `auto` = Instagram graduates it if it performs well. Single-video Reels only: a photo, carousel or story with `trial_reel` is refused (400) rather than published to followers. The account must be eligible for Trial Reels." },
          "audio_name": { "type": "string", "description": "Instagram Reels: display title of the Reel's original audio." },
          "thumb_offset": { "type": "string", "description": "Instagram Reels: cover frame offset, in ms." },

          "threads_reply_control": { "type": "string", "enum": ["everyone", "accounts_you_follow", "mentioned_only", "parent_post_author_only", "followers_only"], "description": "Threads: who can reply. Default `everyone`." },
          "threads_alt_text": { "type": "string", "maxLength": 1000, "description": "Threads: image alt text." },

          "x_reply_settings": { "type": "string", "enum": ["everyone", "following", "mentionedUsers", "subscribers", "verified"], "description": "X: who can reply. Default `everyone`." },
          "x_made_with_ai": { "type": "boolean", "description": "X: label the post as made with AI." },
          "x_paid_partnership": { "type": "boolean", "description": "X: label the post as a paid partnership." },
          "x_alt_text": { "type": "string", "maxLength": 1000, "description": "X: alt text applied to every image. Images only." },
          "x_tagged_users": { "type": "array", "maxItems": 10, "items": { "type": "string", "pattern": "^@?\\w{1,15}$" }, "description": "X: usernames tagged in the photos (multipart: JSON array or comma-separated). Images only. A tag only shows if the person allows photo tagging." },
          "x_subtitles_url": { "type": "string", "format": "uri", "description": "X: SRT or VTT file (max 1 MB) shown as captions. Video only." },
          "x_subtitles_language": { "type": "string", "pattern": "^[a-zA-Z]{2}$", "description": "X: 2-letter language code of the subtitles. Default `EN`. Needs `x_subtitles_url`." },
          "x_poll_options": { "type": "array", "minItems": 2, "maxItems": 4, "items": { "type": "string", "minLength": 1, "maxLength": 25 }, "description": "X: poll choices (multipart: JSON array or comma-separated). Text-only posts: X refuses a poll with media." },
          "x_poll_duration_minutes": { "type": "integer", "minimum": 5, "maximum": 10080, "description": "X: poll duration. Default `1440`. Needs `x_poll_options`." },
          "x_community_id": { "type": "string", "pattern": "^[0-9]{1,19}$", "description": "X: post to this Community." },
          "x_share_with_followers": { "type": "boolean", "description": "X: with `x_community_id`, also show the post to followers." },
          "x_super_followers_only": { "type": "boolean", "description": "X: only Super Followers see the post. The account needs X subscriptions enabled." },
          "x_premium": { "type": "boolean", "description": "X: the account has X Premium, so text up to 25,000 characters instead of 280 and videos up to 125 minutes instead of 20." },

          "visibility": { "type": "string", "enum": ["PUBLIC", "CONNECTIONS"], "description": "LinkedIn." },

          "bluesky_langs": { "type": "string", "description": "Bluesky: comma-separated language codes (e.g. `en,fr`) or a JSON array." },
          "bluesky_labels": { "type": "string", "description": "Bluesky content warnings: `sexual`, `nudity`, `porn`, `graphic-media`, `!no-unauthenticated`." },

          "pinterest_board_id": { "type": "string", "description": "Pinterest: board to pin to." },
          "board_id": { "type": "string", "description": "Pinterest: alias of `pinterest_board_id`." },

          "instagram_as_story": {
            "type": "boolean",
            "description": "Publish to the story of the Instagram accounts in the batch, with the rules of `media_type: STORIES` on `UploadRequest`. A separate switch because `media_type` applies to every platform of the batch and cannot mean \"story\" for Instagram alone; the other platforms publish normally."
          }
        }
      },
      "PostingBucket": {
        "type": "object",
        "description": "One weekday or one hour of day, averaged over the posts that landed in it.",
        "properties": {
          "weekday": { "type": "integer", "minimum": 0, "maximum": 6, "description": "0 = Sunday, following Postgres' `dow`. Present on `byWeekday` and `bestDay`." },
          "hour": { "type": "integer", "minimum": 0, "maximum": 23, "description": "Hour of day in the requested timezone. Present on `byHour` and `bestHours`." },
          "posts": { "type": "integer" },
          "avgViews": { "type": "integer" },
          "ranked": { "type": "boolean", "description": "False when `posts` is under `minSamples`. The bucket keeps its average — it is worth showing — but it is never eligible to win. One lucky post is a coincidence, not a slot, and it is the loudest one: a single viral video gives its bucket an average no honest bucket can beat." }
        }
      },
      "PostingInsights": {
        "type": "object",
        "description": "When this account has published, and what those slots averaged. Derived from the posts already collected, so it says nothing until there are enough of them.",
        "properties": {
          "timezone": { "type": "string", "description": "The zone the buckets were read in — whatever you asked for, or `UTC`." },
          "minSamples": { "type": "integer", "description": "Posts a bucket needs before it can be ranked." },
          "byWeekday": { "type": "array", "description": "Always 7 entries, Sunday first, including the days with nothing in them.", "items": { "$ref": "#/components/schemas/PostingBucket" } },
          "byHour": { "type": "array", "description": "Always 24 entries, 00:00 first.", "items": { "$ref": "#/components/schemas/PostingBucket" } },
          "bestDay": { "oneOf": [{ "$ref": "#/components/schemas/PostingBucket" }, { "type": "null" }] },
          "bestHours": { "type": "array", "description": "Up to 5 hours, best first. Ranked buckets only.", "items": { "$ref": "#/components/schemas/PostingBucket" } },
          "sampleSize": { "type": "integer", "description": "Posts carrying a publication date, i.e. what the buckets were built from." }
        }
      },
      "Analytics": {
        "type": "object",
        "description": "What the collector holds for the accounts in scope. Every metric is a running total read from the platform, not a per-day figure.",
        "properties": {
          "overview": {
            "type": "object",
            "properties": {
              "totalViews": { "type": "integer" },
              "totalLikes": { "type": "integer" },
              "totalComments": { "type": "integer" },
              "totalShares": { "type": "integer" },
              "videoCount": { "type": "integer" },
              "accountCount": { "type": "integer" },
              "bestAccount": {
                "type": ["object", "null"],
                "properties": {
                  "name": { "type": "string" },
                  "platform": { "$ref": "#/components/schemas/Platform" },
                  "views": { "type": "integer" }
                }
              }
            }
          },
          "videos": {
            "type": "array",
            "description": "One row per tracked post, with its metrics, an `isTrending` flag and `source`: `mut` if it was published through Multi Upload Tool, `external` if it was posted directly on the platform.",
            "items": { "type": "object", "additionalProperties": true }
          },
          "bestPerforming": {
            "type": ["object", "null"],
            "properties": {
              "mostViewed": { "type": "object", "additionalProperties": true },
              "mostEngaging": { "type": "object", "additionalProperties": true }
            }
          },
          "topHashtags": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "postingInsights": { "$ref": "#/components/schemas/PostingInsights" },
          "dailyHistory": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "platformHistory": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "summary": {
            "type": "object",
            "description": "The last 30 days against the 30 before. `null` means not measured, never zero.",
            "properties": {
              "windowDays": { "type": "integer", "const": 30 },
              "views": {
                "type": "object",
                "description": "Views gained in each period, summed from the daily series (gaps spread evenly, as on the chart). `measuredDays` says how many days of each period had data.",
                "properties": {
                  "current": { "type": ["integer", "null"] },
                  "previous": { "type": ["integer", "null"] },
                  "change": { "type": ["integer", "null"] },
                  "changePct": { "type": ["number", "null"] },
                  "measuredDays": { "type": "object", "properties": { "current": { "type": "integer" }, "previous": { "type": "integer" } } }
                }
              },
              "followers": {
                "type": "object",
                "description": "`total` = latest follower count summed over accounts. `change` only counts accounts that were also measured 30 days ago, so it stays `null` for the first 30 days of history.",
                "properties": {
                  "total": { "type": ["integer", "null"] },
                  "previous": { "type": ["integer", "null"] },
                  "change": { "type": ["integer", "null"] },
                  "changePct": { "type": ["number", "null"] }
                }
              },
              "postsPublished": {
                "type": "object",
                "properties": { "current": { "type": "integer" }, "previous": { "type": "integer" }, "change": { "type": "integer" } }
              }
            }
          },
          "followerHistory": {
            "type": "array",
            "description": "One point per day over the window: the sum of each account's last known follower count. Days before the first reading are omitted.",
            "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "followers": { "type": "integer" } } }
          },
          "accountTags": {
            "type": "array",
            "description": "Only on the single-account route.",
            "items": { "type": "string" }
          },
          "lastRefreshedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the collector last read the platform. Show it next to the numbers."
          },
          "neverSynced": {
            "type": "boolean",
            "description": "`true` means nothing has ever been collected, as opposed to collected and empty. The two deserve different messages: \"connect and wait for tonight\" is not \"you have no posts yet\"."
          },
          "syncErrors": {
            "type": "array",
            "description": "Accounts whose last collection failed. A failing account never stops the others.",
            "items": {
              "type": "object",
              "properties": {
                "accountName": { "type": "string" },
                "platform": { "$ref": "#/components/schemas/Platform" },
                "error": { "type": "string" }
              }
            }
          }
        }
      },
      "MediaField": {
        "description": "A file part, a publicly reachable URL, or an array of either. File parts must stay under the 100 MB edge cap on the request body; larger media is sent as the `accessUrl` returned by `POST /upload/presign`.",
        "oneOf": [
          { "type": "string", "format": "binary" },
          { "type": "string", "format": "uri" },
          {
            "type": "array",
            "items": {
              "oneOf": [
                { "type": "string", "format": "binary" },
                { "type": "string", "format": "uri" }
              ]
            }
          }
        ]
      }
    }
  }
}
