{
  "openapi": "3.1.0",
  "info": {
    "title": "CreativePDF",
    "version": "1.0.0",
    "summary": "Editar um PDF que já existe, preservando tudo o que não foi editado.",
    "description": "Esta API recusa em vez de degradar em silêncio. Um glifo que a fonte do documento não tem devolve 409 com a lista dos caracteres que faltam, e não um documento com a fonte trocada. Ver saas/PLANO.md."
  },
  "servers": [{ "url": "/" }],
  "security": [{ "bearer": [] }],
  "components": {
    "securitySchemes": {
      "bearer": { "type": "http", "scheme": "bearer" }
    },
    "parameters": {
      "doc": { "name": "doc", "in": "path", "required": true, "schema": { "type": "string" } },
      "ver": { "name": "ver", "in": "path", "required": true, "schema": { "type": "string" }, "description": "ver_1 é o arquivo como entrou, byte a byte." },
      "n": { "name": "n", "in": "path", "required": true, "schema": { "type": "integer", "minimum": 1 } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error", "message"],
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" },
          "missing": { "type": "string", "description": "Em font_missing_glyph: todos os caracteres que faltam, não o primeiro." },
          "font": { "type": "string" },
          "page": { "type": "integer" }
        }
      },
      "Document": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "filename": { "type": "string" },
          "pages": { "type": "integer" },
          "objects": { "type": "integer" },
          "pdf_version": { "type": "string" },
          "xref_recovered": { "type": "boolean" },
          "encrypted": { "type": "boolean" },
          "versions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "bytes": { "type": "integer" },
                "parent": { "type": "string" }
              }
            }
          }
        }
      },
      "Edit": {
        "type": "object",
        "required": ["anchor", "original", "text"],
        "properties": {
          "anchor": { "type": "string", "description": "Opaca e assinada. Veio da estrutura da página; devolva-a como recebeu e nunca a interprete." },
          "original": { "type": "string", "description": "O texto que estava ali. Obrigatório: um índice deslocado não falha sozinho, escreve no lugar errado." },
          "text": { "type": "string" },
          "font_size": { "type": "number", "description": "Corpo novo em pontos. Zero ou ausente mantém o da linha." },
          "font": { "type": "string", "description": "Identificador de uma fonte da sua biblioteca. Instala essa fonte no documento como recurso novo e escreve este trecho com ela — é assim que negrito, itálico e troca de família chegam ao arquivo. Ausente mantém a fonte da linha." },
          "box_left": { "type": "number" },
          "box_right": { "type": "number" },
          "align": { "type": "integer", "minimum": 0, "maximum": 4, "description": "0 desconhecido, 1 esquerda, 2 direita, 3 centro, 4 justificado." }
        }
      },
      "EditResult": {
        "type": "object",
        "properties": {
          "version": { "type": "string" },
          "parent": { "type": "string" },
          "warnings": {
            "type": "object",
            "description": "Não são erro. pushed é quanto o vizinho andou; overflow é quanto passou da coluna. Quem renderiza fatura quer saber antes do cliente dele ver.",
            "properties": {
              "pushed": { "type": "number" },
              "overflow": { "type": "number" }
            }
          }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "job": { "type": "string" },
          "state": { "type": "string", "enum": ["queued", "running", "succeeded", "failed"] },
          "status": { "type": "integer" },
          "result": { "type": "object" }
        }
      }
    }
  },
  "paths": {
    "/v1/health": {
      "get": {
        "summary": "O serviço está de pé.",
        "responses": { "200": { "description": "ok" } }
      }
    },
    "/v1/openapi.json": {
      "get": { "summary": "Este documento.", "responses": { "200": { "description": "ok" } } }
    },
    "/v1/documents": {
      "post": {
        "summary": "Cria o documento a partir dos bytes do PDF.",
        "description": "O corpo é o arquivo. A versão ver_1 é o que entrou, byte a byte — baixá-la e comparar é como o P1 se verifica de fora.",
        "parameters": [
          { "name": "X-Filename", "in": "header", "schema": { "type": "string" } },
          { "name": "Idempotency-Key", "in": "header", "schema": { "type": "string" }, "description": "Repetir com a mesma chave devolve a mesma resposta em vez de criar outro documento." },
          { "name": "Prefer", "in": "header", "schema": { "type": "string", "enum": ["respond-async"] } },
          { "name": "X-Webhook-Url", "in": "header", "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } }
        },
        "responses": {
          "201": { "description": "criado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Document" } } } },
          "202": { "description": "adiado", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "413": { "description": "grande demais", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "não é PDF, ou pede senha", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/documents/{doc}": {
      "parameters": [{ "$ref": "#/components/parameters/doc" }],
      "get": {
        "summary": "Metadados e histórico de versões.",
        "responses": {
          "200": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Document" } } } },
          "404": { "description": "não existe, ou é de outro inquilino" }
        }
      },
      "delete": {
        "summary": "Apaga o documento e todas as versões dele.",
        "responses": { "200": { "description": "apagado" } }
      }
    },
    "/v1/documents/{doc}/versions": {
      "parameters": [{ "$ref": "#/components/parameters/doc" }],
      "get": { "summary": "As versões, em ordem.", "responses": { "200": { "description": "ok" } } }
    },
    "/v1/documents/{doc}/versions/{ver}/file": {
      "parameters": [
        { "$ref": "#/components/parameters/doc" },
        { "$ref": "#/components/parameters/ver" }
      ],
      "get": {
        "summary": "Baixa o PDF daquela versão.",
        "responses": { "200": { "description": "ok", "content": { "application/pdf": { "schema": { "type": "string", "format": "binary" } } } } }
      }
    },
    "/v1/documents/{doc}/versions/{ver}/pages/{n}": {
      "parameters": [
        { "$ref": "#/components/parameters/doc" },
        { "$ref": "#/components/parameters/ver" },
        { "$ref": "#/components/parameters/n" }
      ],
      "get": {
        "summary": "A estrutura da página: linhas, palavras, trechos, tabelas, fontes e mobiliário.",
        "description": "Cada linha e cada palavra que corresponde a um trecho editável traz uma âncora opaca. É o mesmo formato que o editor no navegador consome.",
        "responses": { "200": { "description": "ok" }, "404": { "description": "página ou versão inexistente" } }
      }
    },
    "/v1/documents/{doc}/versions/{ver}/pages/{n}/render": {
      "parameters": [
        { "$ref": "#/components/parameters/doc" },
        { "$ref": "#/components/parameters/ver" },
        { "$ref": "#/components/parameters/n" },
        { "name": "dpi", "in": "query", "schema": { "type": "number", "minimum": 12, "maximum": 600 } }
      ],
      "get": {
        "summary": "A página rasterizada, em PNG.",
        "description": "O mesmo rasterizador do editor. Renderize a versão nova e a anterior e compare: o que mudou tem de ser só o que você pediu.",
        "responses": { "200": { "description": "ok", "content": { "image/png": { "schema": { "type": "string", "format": "binary" } } } } }
      }
    },
    "/v1/documents/{doc}/versions/{ver}/edits": {
      "parameters": [
        { "$ref": "#/components/parameters/doc" },
        { "$ref": "#/components/parameters/ver" }
      ],
      "post": {
        "summary": "Aplica trocas de texto e cria uma versão nova.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["edits"],
                "properties": { "edits": { "type": "array", "items": { "$ref": "#/components/schemas/Edit" } } }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "versão criada", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EditResult" } } } },
          "400": { "description": "JSON inválido, ou âncora ilegível/adulterada" },
          "409": { "description": "âncora de outra versão (stale_anchor), ou glifo ausente (font_missing_glyph)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "a edição não pôde ser aplicada" }
        }
      }
    },
    "/v1/documents/{doc}/versions/{ver}/fonts/{resource}/glyphs": {
      "parameters": [
        { "$ref": "#/components/parameters/doc" },
        { "$ref": "#/components/parameters/ver" },
        { "name": "resource", "in": "path", "required": true, "schema": { "type": "string" }, "description": "O nome do recurso de fonte na página, como a estrutura o reporta: F1, R13, TT0." }
      ],
      "post": {
        "summary": "Acrescenta glifos ao subset embarcado, tirando os contornos de um doador.",
        "description": "É o que destrava o P4 quando a fonte do documento não tem o caractere. O doador precisa ter a mesma escala de em.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["donor", "characters"],
                "properties": {
                  "donor": { "type": "string", "description": "Identificador de uma fonte da biblioteca do inquilino." },
                  "characters": { "type": "string" },
                  "page": { "type": "integer", "minimum": 1 }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "versão criada" },
          "404": { "description": "doador não instalado" },
          "422": { "description": "doador recusado: outra escala de em, ou formato incompatível" }
        }
      }
    },
    "/v1/fonts": {
      "get": { "summary": "A biblioteca do inquilino.", "responses": { "200": { "description": "ok" } } },
      "post": {
        "summary": "Instala um arquivo de fonte.",
        "description": "TrueType, OpenType ou CFF nu. A biblioteca é por inquilino e nunca compartilhada: licença de fonte é por licenciado.",
        "parameters": [{ "name": "X-Font-Name", "in": "header", "schema": { "type": "string" } }],
        "requestBody": { "required": true, "content": { "font/ttf": { "schema": { "type": "string", "format": "binary" } } } },
        "responses": {
          "201": { "description": "instalada" },
          "422": { "description": "não é fonte" }
        }
      }
    },
    "/v1/fonts/{font}": {
      "parameters": [{ "name": "font", "in": "path", "required": true, "schema": { "type": "string" } }],
      "delete": { "summary": "Remove a fonte.", "responses": { "200": { "description": "removida" }, "404": { "description": "não existe" } } }
    },
    "/v1/jobs/{job}": {
      "parameters": [{ "name": "job", "in": "path", "required": true, "schema": { "type": "string" } }],
      "get": {
        "summary": "O estado de um trabalho adiado, e o resultado quando houver.",
        "responses": {
          "200": { "description": "ok", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "404": { "description": "não existe, ou é de outro inquilino" }
        }
      }
    }
  }
}
