{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Publish Zuola Integration API",
    "version": "0.6.0",
    "description": "影片、封面、字幕與各平台文案交付，以及非同步多平台排程。對外 Token 僅能操作自己的交付包與允許帳號；不開放管理後台、留言或任意純文字草稿 API。MCP 提供同 scope 的只讀查詢。文件公開，Token 由站長在後台建立。",
    "contact": {
      "name": "Zuola",
      "url": "https://publish.zuola.com/inquiry"
    }
  },
  "servers": [
    {
      "url": "https://publish.zuola.com"
    }
  ],
  "tags": [
    {
      "name": "Delivery",
      "description": "Application-scoped durable media delivery and explicit scheduling."
    },
    {
      "name": "MCP",
      "description": "Read-only assistant discovery and package monitoring."
    }
  ],
  "security": [
    {
      "ApplicationToken": []
    }
  ],
  "paths": {
    "/api/v1/integrations/clip/capabilities": {
      "get": {
        "operationId": "getCapabilities",
        "tags": [
          "Delivery"
        ],
        "summary": "取得可用帳號與限制",
        "description": "Returns only channels allowed to the calling application. Capability support is not a guarantee that a particular account has successfully published.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capabilities"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        }
      }
    },
    "/api/v1/integrations/clip/packages": {
      "post": {
        "operationId": "createPackage",
        "tags": [
          "Delivery"
        ],
        "summary": "建立影片交付包",
        "description": "Uploads and scheduling are separate. source_kind=original is a trusted provenance declaration, never a way to bypass Feed lineage. Same immutable external_id/request returns the existing package.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Package"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "200": {
            "description": "Existing package replayed; no duplicate delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Package"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "1–120 characters. Stable for the same operation and immutable request; reuse after network errors, never generate a new key on each retry. Namespaced per application.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Manifest"
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/clip/packages/{id}": {
      "get": {
        "operationId": "getPackage",
        "tags": [
          "Delivery"
        ],
        "summary": "查上傳進度、驗證與發布結果",
        "description": "Poll at a bounded interval, e.g. 5 seconds. storage_committed=true and state=ready are required before scheduling. Inspect each target state and waiting_dependency separately.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Package"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact package ID from POST /packages."
          }
        ]
      }
    },
    "/api/v1/integrations/clip/packages/{id}/files/{key}": {
      "put": {
        "operationId": "uploadFileChunk",
        "tags": [
          "Delivery"
        ],
        "summary": "續傳一段二進位檔案",
        "description": "Send bytes from the declared file, not base64 or multipart. Each chunk <= chunk_bytes (currently 8 MiB). Same completed byte range with identical bytes is safe to replay. On 409 read expected_offset or GET the package; never blindly resend different content.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offset"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "413": {
            "$ref": "#/components/responses/Error413"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "507": {
            "$ref": "#/components/responses/Error507"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact package ID from POST /packages."
          },
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,80}$"
            }
          },
          {
            "name": "Content-Range",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^bytes [0-9]+-[0-9]+/[0-9]+$"
            },
            "example": "bytes 0-1023/1024"
          },
          {
            "name": "Content-Length",
            "in": "header",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 8388608
            },
            "description": "Exact chunk byte count; required even if the HTTP client defaults to chunked transfer."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/clip/packages/{id}/complete": {
      "post": {
        "operationId": "completePackage",
        "tags": [
          "Delivery"
        ],
        "summary": "完成上傳並開始驗證",
        "description": "202 means validation accepted, not storage committed or content published. All file offsets must be complete. Keep polling GET /packages/{id}; use its latest version when scheduling.",
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Package"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact package ID from POST /packages."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "1–120 characters. Stable for the same operation and immutable request; reuse after network errors, never generate a new key on each retry. Namespaced per application.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Complete"
              }
            }
          }
        }
      }
    },
    "/api/v1/integrations/clip/packages/{id}/schedule": {
      "post": {
        "operationId": "schedulePackage",
        "tags": [
          "Delivery"
        ],
        "summary": "建立立即、指定時間或佇列發布任務",
        "description": "This is the only service API operation that authorizes publication and queues task notifications. Call only after explicit user approval of content, channels and time. mode=now is asynchronous, not proof of publication. WordPress requires a named application, original provenance and YouTube in the same plan; Feed-derived content can never return to WordPress. Uses YouTube → WordPress → other platforms order.",
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scheduled"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error400"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "$ref": "#/components/responses/Error409"
          },
          "422": {
            "$ref": "#/components/responses/Error422"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "500": {
            "$ref": "#/components/responses/Error500"
          },
          "502": {
            "$ref": "#/components/responses/Error502"
          },
          "200": {
            "description": "Same scheduling request replayed; no new tasks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scheduled"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact package ID from POST /packages."
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "1–120 characters. Stable for the same operation and immutable request; reuse after network errors, never generate a new key on each retry. Namespaced per application.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Schedule"
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcpRequest",
        "tags": [
          "MCP"
        ],
        "summary": "MCP Streamable HTTP（唯讀）",
        "description": "Stateless JSON responses. Exposes publish_capabilities, publish_package_status and publish://integration. Requires the same application Bearer token on every request. No upload, scheduling, reply, OAuth discovery or standalone SSE stream. Clients must support custom Authorization headers. Initialize with a supported protocol version such as 2025-11-25, then send MCP-Protocol-Version. HTTP 200 with tools/call result.isError=true is a tool failure, not success. Origin, when present, must match the Publish origin. Request limit: 64 KiB.",
        "parameters": [
          {
            "name": "Accept",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "application/json, text/event-stream"
          },
          {
            "name": "MCP-Protocol-Version",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "example": "2025-11-25",
            "description": "Use the negotiated version after initialize."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JSONRPCRequest"
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "initialize",
                "params": {
                  "protocolVersion": "2025-11-25",
                  "capabilities": {},
                  "clientInfo": {
                    "name": "example-client",
                    "version": "1.0.0"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response; inspect result.isError for tool errors.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JSONRPCResponse"
                }
              }
            }
          },
          "202": {
            "description": "Accepted notification; empty body."
          },
          "401": {
            "description": "Missing, invalid or revoked application Bearer token. Browser-origin REST calls are rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Application rate limit exceeded; obey Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds until retry; currently 60.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "400": {
            "description": "Invalid MCP request/version/Accept header; may be plain text."
          },
          "403": {
            "description": "Invalid Origin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "MCP body exceeds 64 KiB; may be plain text."
          },
          "415": {
            "description": "Content-Type must be application/json; may be plain text."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApplicationToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pub_app_…",
        "description": "Owner-created application token. Store only in backend secrets. Do not use Google login cookies or pass tokens via query strings. API and MCP share a per-application limit of 600 requests/minute."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable error code; inspect HTTP status first."
          },
          "expected_offset": {
            "type": "integer",
            "minimum": 0
          },
          "upstream": {
            "type": "object",
            "description": "Optional sanitized shortener failure details."
          },
          "issues": {
            "type": "array",
            "description": "For platform_text_limit: all affected accounts, each with field, actual count including any distribution reserve, and limit. No publishing targets are created; review all account versions before retrying.",
            "items": {
              "type": "object",
              "properties": {
                "channel_id": {
                  "type": "string"
                },
                "platform": {
                  "type": "string"
                },
                "channel_name": {
                  "type": "string"
                },
                "field": {
                  "type": "string"
                },
                "count": {
                  "type": "integer"
                },
                "limit": {
                  "type": "integer"
                },
                "unit": {
                  "type": "string"
                },
                "reserved": {
                  "type": "integer"
                }
              },
              "required": [
                "channel_id",
                "platform",
                "channel_name",
                "field",
                "count",
                "limit",
                "unit",
                "reserved"
              ]
            }
          }
        },
        "required": [
          "error"
        ]
      },
      "Content": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "At most 500 UTF-8 bytes; platform limits also apply."
          },
          "body": {
            "type": "string",
            "description": "At most 200000 UTF-8 bytes; not automatically truncated."
          },
          "language": {
            "type": "string"
          },
          "internal_tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "hashtags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": false
      },
      "File": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "role": {
            "type": "string",
            "enum": [
              "video",
              "cover",
              "subtitle"
            ]
          },
          "mime": {
            "type": "string",
            "description": "Declared MIME; the server verifies actual file content. Use video/mp4, image/jpeg, image/png or application/x-subrip."
          },
          "bytes": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2147483648,
            "description": "Video <= 2 GiB, cover <= 25 MiB, subtitle <= 100000 bytes."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[a-f0-9]{64}$"
          },
          "rendition_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "language": {
            "type": "string",
            "description": "Required for subtitles, e.g. zh-Hant or en."
          },
          "video_key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "burned_in_subtitles": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "key",
          "role",
          "bytes",
          "sha256",
          "rendition_id"
        ],
        "additionalProperties": false
      },
      "Version": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "provider": {
            "type": "string",
            "enum": [
              "mastodon",
              "telegram",
              "x",
              "facebook",
              "threads",
              "instagram",
              "bluesky",
              "youtube",
              "wordpress"
            ]
          },
          "title": {
            "type": "string",
            "description": "Optional replacement; otherwise inherits content.title."
          },
          "body": {
            "type": "string",
            "description": "Optional replacement; otherwise inherits content.body."
          },
          "video_key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "cover_key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          },
          "cover_as_photo": {
            "type": "boolean",
            "default": false,
            "description": "Explicit X cover photo reply or Threads carousel. Only x/threads accept true; requires cover_key. Check video_cover_photo capability. Instagram/Telegram cover_key already uses mixed media; do not set this flag there."
          },
          "subtitle_keys": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_-]{1,80}$"
            }
          },
          "hashtags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 20
          },
          "search_tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 100
          },
          "topic_tag": {
            "type": "string",
            "maxLength": 100
          },
          "location": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "key",
          "provider",
          "video_key"
        ],
        "additionalProperties": false
      },
      "Manifest": {
        "type": "object",
        "properties": {
          "schema_version": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Immutable delivery version, unique per application."
          },
          "project_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "rendition_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "source_kind": {
            "type": "string",
            "enum": [
              "original",
              "feed",
              "unknown"
            ],
            "default": "unknown",
            "description": "Truthful provenance, not a permission override. Feed lineage always blocks WordPress."
          },
          "source_refs": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 20,
            "description": "Only opaque source references issued by Publish."
          },
          "canonical_url": {
            "type": "string",
            "description": "Optional public original article URL. Never put private asset download URLs here."
          },
          "content": {
            "$ref": "#/components/schemas/Content"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/File"
            },
            "minItems": 1,
            "maxItems": 32
          },
          "versions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Version"
            },
            "minItems": 1,
            "maxItems": 30
          }
        },
        "required": [
          "schema_version",
          "external_id",
          "project_id",
          "rendition_id",
          "files",
          "versions"
        ],
        "additionalProperties": false
      },
      "Capability": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "mastodon",
              "telegram",
              "x",
              "facebook",
              "threads",
              "instagram",
              "bluesky",
              "youtube",
              "wordpress"
            ]
          },
          "name": {
            "type": "string"
          },
          "configured": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "text_limit": {
            "type": "integer"
          },
          "text_unit": {
            "type": "string"
          },
          "max_images": {
            "type": "integer"
          },
          "video": {
            "type": "boolean"
          },
          "native_subtitles": {
            "type": "boolean"
          },
          "thumbnail": {
            "type": "boolean"
          },
          "cover_as_photo": {
            "type": "boolean"
          },
          "text_only": {
            "type": "boolean"
          },
          "video_cover_photo": {
            "type": "string",
            "enum": [
              "reply",
              "carousel"
            ],
            "description": "Optional: explicit Clip cover behavior on X/Threads."
          },
          "url_characters": {
            "type": "integer"
          },
          "checked_at": {
            "type": "integer",
            "description": "Unix milliseconds; zero means not dynamically checked."
          },
          "notes": {
            "type": "string"
          }
        },
        "required": [
          "channel_id",
          "provider",
          "name",
          "configured",
          "enabled",
          "text_limit",
          "max_images",
          "video",
          "cover_as_photo",
          "text_only"
        ]
      },
      "Capabilities": {
        "type": "object",
        "properties": {
          "schema_version": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "application_id": {
            "type": "string"
          },
          "channels": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Capability"
            }
          },
          "schedule_modes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "now",
                "at",
                "queue"
              ]
            }
          },
          "wordpress_requires_original_and_youtube": {
            "type": "boolean"
          },
          "shortener_configured": {
            "type": "boolean"
          },
          "max_manifest_bytes": {
            "type": "integer"
          },
          "max_files": {
            "type": "integer"
          },
          "chunk_bytes": {
            "type": "integer"
          },
          "max_package_bytes": {
            "type": "integer"
          },
          "max_file_bytes_by_role": {
            "type": "object",
            "properties": {
              "video": {
                "type": "integer"
              },
              "cover": {
                "type": "integer"
              },
              "subtitle": {
                "type": "integer"
              }
            },
            "required": [
              "video",
              "cover",
              "subtitle"
            ]
          }
        },
        "required": [
          "channels",
          "application_id",
          "schedule_modes",
          "chunk_bytes",
          "max_file_bytes_by_role"
        ]
      },
      "UploadedFile": {
        "type": "object",
        "properties": {
          "file_key": {
            "type": "string"
          },
          "offset": {
            "type": "integer",
            "minimum": 0
          },
          "asset_id": {
            "type": "string"
          },
          "upload_path": {
            "type": "string",
            "description": "Relative PUT path on the same Publish origin. Never forward credentials to another origin."
          },
          "chunk_bytes": {
            "type": "integer"
          }
        },
        "required": [
          "file_key",
          "offset",
          "asset_id",
          "upload_path",
          "chunk_bytes"
        ]
      },
      "DeliveryTarget": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "channel_id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "scheduled, publishing, published, retry_wait, failed, unknown, needs_reauth, cancelled, expired or policy_blocked."
          },
          "scheduled_at": {
            "type": "integer",
            "description": "Unix milliseconds; earliest eligible start, not guaranteed public time."
          },
          "last_error": {
            "type": "string"
          },
          "remote_id": {
            "type": "string"
          },
          "remote_url": {
            "type": "string"
          },
          "waiting_dependency": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "channel_id",
          "state",
          "scheduled_at",
          "last_error",
          "waiting_dependency"
        ]
      },
      "Package": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "external_id": {
            "type": "string"
          },
          "manifest_sha256": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "minimum": 1
          },
          "state": {
            "type": "string",
            "enum": [
              "uploading",
              "validating",
              "validation_failed",
              "ready"
            ],
            "description": "Storage lifecycle. Read targets for publishing state; ready alone never means published."
          },
          "storage_committed": {
            "type": "boolean"
          },
          "post_id": {
            "type": "string"
          },
          "last_error": {
            "type": "string"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UploadedFile"
            }
          },
          "targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeliveryTarget"
            }
          }
        },
        "required": [
          "id",
          "external_id",
          "manifest_sha256",
          "version",
          "state",
          "storage_committed",
          "post_id",
          "last_error",
          "files",
          "targets"
        ]
      },
      "Complete": {
        "type": "object",
        "properties": {
          "expected_version": {
            "type": "integer",
            "minimum": 1
          }
        },
        "required": [
          "expected_version"
        ],
        "additionalProperties": false
      },
      "ScheduleTarget": {
        "type": "object",
        "properties": {
          "channel_id": {
            "type": "string",
            "minLength": 1
          },
          "version_key": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9_-]{1,80}$"
          }
        },
        "required": [
          "channel_id",
          "version_key"
        ],
        "additionalProperties": false
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "expected_version": {
            "type": "integer",
            "minimum": 1
          },
          "mode": {
            "type": "string",
            "enum": [
              "now",
              "at",
              "queue"
            ]
          },
          "scheduled_at": {
            "type": "string",
            "description": "For at: a future RFC3339 timestamp with explicit offset; omit for now/queue."
          },
          "timezone": {
            "type": "string",
            "description": "For at: IANA name whose offset matches scheduled_at, e.g. Asia/Taipei or UTC."
          },
          "targets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleTarget"
            },
            "minItems": 1,
            "maxItems": 30
          }
        },
        "required": [
          "expected_version",
          "mode",
          "targets"
        ],
        "additionalProperties": false,
        "allOf": [
          {
            "if": {
              "properties": {
                "mode": {
                  "const": "at"
                }
              },
              "required": [
                "mode"
              ]
            },
            "then": {
              "required": [
                "scheduled_at",
                "timezone"
              ]
            }
          }
        ]
      },
      "Scheduled": {
        "type": "object",
        "properties": {
          "targets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "channel_id": {
                  "type": "string"
                },
                "scheduled_at": {
                  "type": "integer"
                }
              },
              "required": [
                "id",
                "channel_id",
                "scheduled_at"
              ]
            }
          }
        },
        "required": [
          "targets"
        ]
      },
      "Offset": {
        "type": "object",
        "properties": {
          "offset": {
            "type": "integer",
            "minimum": 0
          }
        },
        "required": [
          "offset"
        ]
      },
      "JSONRPCRequest": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "type": [
              "string",
              "integer"
            ]
          },
          "method": {
            "type": "string"
          },
          "params": {
            "type": "object"
          }
        },
        "required": [
          "jsonrpc",
          "method"
        ]
      },
      "JSONRPCResponse": {
        "type": "object",
        "properties": {
          "jsonrpc": {
            "type": "string",
            "enum": [
              "2.0"
            ]
          },
          "id": {
            "type": [
              "string",
              "integer",
              "null"
            ]
          },
          "result": {
            "type": "object"
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "integer"
              },
              "message": {
                "type": "string"
              },
              "data": {}
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "jsonrpc",
          "id"
        ]
      }
    },
    "responses": {
      "Error400": {
        "description": "Invalid JSON or unknown fields.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error401": {
        "description": "Missing, invalid or revoked application Bearer token. Browser-origin REST calls are rejected.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error404": {
        "description": "Package/file not found or belongs to another application; indistinguishable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error429": {
        "description": "Application rate limit exceeded; obey Retry-After.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "description": "Seconds until retry; currently 60.",
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "Error500": {
        "description": "Internal failure; retain receipt and keys, inspect current state before retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error409": {
        "description": "Version, offset, readiness or idempotency conflict. Inspect error before retrying.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error413": {
        "description": "Upload/request limit exceeded.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error422": {
        "description": "Invalid manifest, range, time, media or source policy.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error507": {
        "description": "Insufficient storage or durable write failed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error403": {
        "description": "Selected channel is outside this application allowlist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Error502": {
        "description": "Shortener temporarily unavailable or rejected the request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "人類與 AI 開發者指南",
    "url": "https://publish.zuola.com/developers"
  }
}
