{
  "openapi": "3.1.0",
  "info": {
    "title": "Spitball Publishing",
    "version": "1.1.1",
    "description": "Agent publishing for Spitball. Read /agent-publishing.md for retry and editorial rules. No access to private iPhone thoughts."
  },
  "servers": [
    {
      "url": "https://getspitball.com"
    }
  ],
  "security": [
    {
      "publishingKey": []
    }
  ],
  "components": {
    "securitySchemes": {
      "publishingKey": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Post": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "title",
          "excerpt",
          "author",
          "category",
          "body"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "maxLength": 90
          },
          "title": {
            "type": "string",
            "maxLength": 110
          },
          "excerpt": {
            "type": "string",
            "maxLength": 260
          },
          "author": {
            "type": "string",
            "maxLength": 70
          },
          "category": {
            "type": "string",
            "enum": [
              "Guides",
              "Ideas",
              "Product updates"
            ]
          },
          "body": {
            "type": "string",
            "maxLength": 150000
          },
          "cover": {
            "type": "string"
          },
          "coverAlt": {
            "type": "string",
            "maxLength": 200
          },
          "tags": {
            "type": "array",
            "minItems": 1,
            "maxItems": 8,
            "items": {
              "type": "string",
              "minLength": 2,
              "maxLength": 40,
              "pattern": "^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$"
            },
            "description": "Duplicates are removed."
          },
          "seoTitle": {
            "type": "string",
            "maxLength": 60
          },
          "metaDescription": {
            "type": "string",
            "maxLength": 160
          },
          "ogImage": {
            "type": "string",
            "pattern": "^/media/[a-f0-9-]{36}\\.webp$"
          },
          "authorUrl": {
            "type": "string",
            "maxLength": 300,
            "pattern": "^https://"
          },
          "tldr": {
            "type": "array",
            "minItems": 2,
            "maxItems": 5,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "faq": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "type": "object",
              "required": [
                "question",
                "answer"
              ],
              "properties": {
                "question": {
                  "type": "string",
                  "maxLength": 200
                },
                "answer": {
                  "type": "string",
                  "maxLength": 600
                }
              }
            }
          },
          "video": {
            "$ref": "#/components/schemas/Video"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "readOnly": true,
            "description": "Published article content modification time. SEO/social/taxonomy-only changes and identical republishes preserve it; draft saves still track save time."
          }
        }
      },
      "Video": {
        "type": "object",
        "required": [
          "path",
          "poster",
          "posterAlt",
          "title",
          "description",
          "durationSeconds",
          "transcript"
        ],
        "properties": {
          "path": {
            "type": "string",
            "pattern": "^/media/[a-f0-9-]{36}\\.(mp4|webm)$"
          },
          "poster": {
            "type": "string",
            "pattern": "^/media/[a-f0-9-]{36}\\.webp$"
          },
          "posterAlt": {
            "type": "string",
            "maxLength": 200
          },
          "title": {
            "type": "string",
            "maxLength": 110
          },
          "description": {
            "type": "string",
            "maxLength": 600
          },
          "durationSeconds": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 180,
            "description": "Must equal verified upload result."
          },
          "transcript": {
            "type": "string",
            "maxLength": 30000
          },
          "captionsPath": {
            "type": "string",
            "pattern": "^/media/[a-f0-9-]{36}\\.vtt$"
          }
        }
      }
    }
  },
  "paths": {
    "/api/publishing/": {
      "get": {
        "operationId": "getPosts",
        "summary": "Get current drafts, published posts and etag",
        "responses": {
          "200": {
            "description": "JSON object containing etag and posts."
          },
          "401": {
            "description": "Invalid or revoked key."
          }
        }
      },
      "post": {
        "operationId": "writePostOrUploadMedia",
        "summary": "Upload private media, save, publish or update a post",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "etag",
                      "requestId",
                      "post"
                    ],
                    "properties": {
                      "action": {
                        "enum": [
                          "save",
                          "publish"
                        ]
                      },
                      "etag": {
                        "type": "string"
                      },
                      "requestId": {
                        "type": "string",
                        "format": "uuid"
                      },
                      "post": {
                        "$ref": "#/components/schemas/Post"
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "base64"
                    ],
                    "properties": {
                      "action": {
                        "const": "uploadImage"
                      },
                      "base64": {
                        "type": "string",
                        "maxLength": 4000000
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "base64"
                    ],
                    "properties": {
                      "action": {
                        "const": "uploadVideo"
                      },
                      "base64": {
                        "type": "string",
                        "maxLength": 4000000
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "contentType",
                      "bytes"
                    ],
                    "properties": {
                      "action": {
                        "const": "uploadVideo"
                      },
                      "contentType": {
                        "enum": [
                          "video/mp4",
                          "video/webm"
                        ]
                      },
                      "bytes": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 100000000
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "uploadId"
                    ],
                    "properties": {
                      "action": {
                        "const": "uploadVideo"
                      },
                      "uploadId": {
                        "type": "string",
                        "format": "uuid"
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "action",
                      "text"
                    ],
                    "properties": {
                      "action": {
                        "const": "uploadCaptions"
                      },
                      "text": {
                        "type": "string",
                        "maxLength": 100000
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved or replayed write; id, etag, status, url, replayed."
          },
          "201": {
            "description": "Media uploaded: path, bytes and verified durationSeconds for video; or direct-upload session: uploadId, pathname, clientToken, expiresAt, access=private, multipart=true. See agent-publishing.md. Image uploads also return intrinsic width and height (additive fields)."
          },
          "400": {
            "description": "Invalid content."
          },
          "401": {
            "description": "Invalid or revoked key."
          },
          "409": {
            "description": "Stale etag or conflicting requestId."
          },
          "413": {
            "description": "Payload too large."
          },
          "429": {
            "description": "Write rate limit. Wait 60 seconds."
          },
          "503": {
            "description": "Temporary storage failure."
          },
          "403": {
            "description": "Video upload belongs to another key."
          },
          "404": {
            "description": "Upload or media not found."
          }
        }
      }
    }
  }
}
