{
  "openapi": "3.0.3",
  "info": {
    "title": "Veracitas API",
    "description": "API pública y B2B de Veracitas (veracitas.es), detector de texto generado por IA en español y sello criptográfico verificable.\n\n**Público (sin auth):** `GET /api/verify` consulta un sello por UUID.\n**B2B (Bearer o X-Api-Key):** `/api/v1/submissions` crea y lista envíos de una cuenta de portal. El token se entrega al aprobar el acceso (https://veracitas.es/solicitar-acceso).\n\nErrores: JSON `{ok, error, message, status, code, hint, docs, openapi, timestamp}`.\nNo documentamos workers internos ni paneles de administración.",
    "version": "1.0.0",
    "contact": {
      "name": "Veracitas — Ready For Metaverse",
      "email": "contacto@veracitas.es",
      "url": "https://veracitas.es/developers"
    },
    "license": {
      "name": "Uso del servicio según aviso legal",
      "url": "https://veracitas.es/legal"
    }
  },
  "servers": [
    {
      "url": "https://veracitas.es",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "public",
      "description": "Verificación pública de un Sello Veracitas. Sin autenticación."
    },
    {
      "name": "b2b",
      "description": "Envíos de una cuenta de portal. Requiere token Bearer."
    }
  ],
  "paths": {
    "/api/verify": {
      "get": {
        "operationId": "verifySeal",
        "tags": ["public"],
        "summary": "Verificar un sello por query",
        "description": "Devuelve el estado público de un Sello Veracitas. Cualquiera puede llamar este endpoint con el UUID del sello. Sin `full_access` (dueño del portal o invitado con código) el informe técnico se recorta: no hay SHA-256 del manuscrito ni heatmap.",
        "parameters": [
          {
            "name": "cert_id",
            "in": "query",
            "required": true,
            "description": "Identificador UUIDv4 del sello.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sello encontrado. `verified` es true.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifySuccess"
                }
              }
            }
          },
          "400": {
            "description": "cert_id no es un UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "ok": false,
                  "error": "cert_id must be a UUIDv4",
                  "code": "invalid_cert_id",
                  "status": 400
                }
              }
            }
          },
          "404": {
            "description": "No hay sello con ese UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                },
                "example": {
                  "ok": false,
                  "error": "cert_id not found",
                  "code": "cert_not_found",
                  "status": 404
                }
              }
            }
          },
          "405": {
            "description": "Método distinto de GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/verify/{cert_id}": {
      "get": {
        "operationId": "verifySealByPath",
        "tags": ["public"],
        "summary": "Verificar un sello por ruta",
        "description": "Igual que `verifySeal`, con el UUID en la ruta. Forma preferida para agentes.",
        "parameters": [
          {
            "name": "cert_id",
            "in": "path",
            "required": true,
            "description": "Identificador UUIDv4 del sello.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sello encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifySuccess"
                }
              }
            }
          },
          "400": {
            "description": "UUID inválido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          },
          "404": {
            "description": "Sello no encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/submissions": {
      "get": {
        "operationId": "listSubmissions",
        "tags": ["b2b"],
        "summary": "Listar envíos de la cuenta",
        "description": "Lista los envíos del perfil autenticado. Filtra por estado, plan o texto. `limit` máximo 200.",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyAuth": [] }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estado de análisis (`queued`, `processing`, `done`, `error`, …).",
            "schema": { "type": "string", "pattern": "^[a-z_]+$" }
          },
          {
            "name": "plan",
            "in": "query",
            "required": false,
            "description": "Plan del envío.",
            "schema": { "type": "string", "enum": ["basic", "premium"] }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Búsqueda en mensaje, nombre o submission_id.",
            "schema": { "type": "string" }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Campo de orden.",
            "schema": { "type": "string", "enum": ["date", "words"], "default": "date" }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["asc", "desc"], "default": "desc" }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "Listado paginado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionList"
                }
              }
            }
          },
          "401": {
            "description": "Token ausente, inválido o perfil desactivado.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createSubmission",
        "tags": ["b2b"],
        "summary": "Crear un envío",
        "description": "Sube un manuscrito (multipart) y lanza el análisis. Mínimo 1.500 palabras para analizar; 3.000 para sello. Formatos: txt, pdf, doc, docx. El token identifica la cuenta; no hay subida anónima pública.",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyAuth": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Manuscrito (.txt, .pdf, .doc, .docx), máximo 50 MB."
                  },
                  "title": { "type": "string", "description": "Título de la obra." },
                  "author": { "type": "string", "description": "Nombre del autor." },
                  "author_email": {
                    "type": "string",
                    "format": "email",
                    "description": "Obligatorio si plan=premium (sello)."
                  },
                  "genre": {
                    "type": "string",
                    "description": "Género o tipo textual (académico, ensayo, …)."
                  },
                  "plan": {
                    "type": "string",
                    "enum": ["basic", "premium"],
                    "default": "basic",
                    "description": "basic = análisis; premium = análisis + sello (revisión humana)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Envío creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionCreated"
                }
              }
            }
          },
          "400": {
            "description": "Falta el archivo u otros campos.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "401": {
            "description": "Token inválido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "413": {
            "description": "Archivo demasiado grande.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "429": {
            "description": "Cuota mensual o rate limit.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          }
        }
      }
    },
    "/api/v1/submissions/{sid}": {
      "get": {
        "operationId": "getSubmission",
        "tags": ["b2b"],
        "summary": "Estado de un envío",
        "description": "Detalle de un envío de la cuenta autenticada, con URLs de verificación si ya hay `cert_id`.",
        "security": [
          { "bearerAuth": [] },
          { "apiKeyAuth": [] }
        ],
        "parameters": [
          {
            "name": "sid",
            "in": "path",
            "required": true,
            "description": "UUIDv4 del envío (`submission_id`).",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Envío encontrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionDetail"
                }
              }
            }
          },
          "400": {
            "description": "sid inválido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "401": {
            "description": "Token inválido.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          },
          "404": {
            "description": "Envío no encontrado en este perfil.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ApiError" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API token",
        "description": "Token del portal editorial. Header `Authorization: Bearer <token>`."
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Alternativa al Bearer. Mismo token de cuenta."
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": ["ok", "error", "message", "status", "code", "hint"],
        "properties": {
          "ok": { "type": "boolean", "enum": [false] },
          "error": { "type": "string", "description": "Mensaje de error (alias estable)." },
          "message": { "type": "string", "description": "Mismo texto que `error`." },
          "status": { "type": "integer", "description": "Código HTTP." },
          "code": {
            "type": "string",
            "description": "Código estable para agentes (`cert_not_found`, `invalid_cert_id`, `invalid_api_token`, `not_found`, …)."
          },
          "hint": { "type": "string", "description": "Qué hacer a continuación." },
          "docs": { "type": "string", "format": "uri" },
          "openapi": { "type": "string", "format": "uri" },
          "timestamp": { "type": "string", "format": "date-time" },
          "path": { "type": "string" },
          "cert_id": { "type": "string", "format": "uuid" }
        }
      },
      "VerifySuccess": {
        "type": "object",
        "required": ["ok", "verified", "cert_id", "seal_status"],
        "properties": {
          "ok": { "type": "boolean" },
          "verified": { "type": "boolean" },
          "full_access": { "type": "boolean" },
          "private_report": { "type": "boolean" },
          "cert_id": { "type": "string", "format": "uuid" },
          "ts_utc": { "type": "string" },
          "original_decision": { "type": "string" },
          "effective_decision": { "type": "string" },
          "seal_status": {
            "type": "string",
            "description": "Estado público del sello (`valid`, `rejected`, `uncertain`, `revoked`, …)."
          },
          "plan": { "type": "string" },
          "genre": { "type": "string", "nullable": true },
          "text_words": { "type": "integer", "nullable": true },
          "text_sha256": {
            "type": "string",
            "nullable": true,
            "description": "Solo con full_access. El público ve null."
          },
          "summary": { "type": "object", "additionalProperties": true },
          "certificate": { "type": "object", "nullable": true, "additionalProperties": true },
          "revoked": { "type": "boolean" },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "Submission": {
        "type": "object",
        "required": ["sid"],
        "properties": {
          "sid": { "type": "string", "format": "uuid" },
          "title": { "type": "string" },
          "author": { "type": "string" },
          "genre": { "type": "string" },
          "plan": { "type": "string" },
          "submitted_at": { "type": "string" },
          "word_count": { "type": "integer", "nullable": true },
          "analysis_status": { "type": "string" },
          "analysis_decision": { "type": "string", "nullable": true },
          "analysis_score": { "type": "number", "nullable": true },
          "analysis_error": { "type": "string", "nullable": true },
          "cert_id": { "type": "string", "format": "uuid", "nullable": true },
          "verify_url_public": { "type": "string", "format": "uri", "nullable": true },
          "verify_url_private": { "type": "string", "format": "uri", "nullable": true },
          "certificate_pdf": { "type": "string", "format": "uri", "nullable": true },
          "seal": { "type": "object", "additionalProperties": true }
        }
      },
      "SubmissionList": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "total": { "type": "integer" },
          "limit": { "type": "integer" },
          "offset": { "type": "integer" },
          "usage_month": { "type": "integer" },
          "quota_month": { "type": "integer" },
          "submissions": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Submission" }
          },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "SubmissionDetail": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "submission": { "$ref": "#/components/schemas/Submission" },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "SubmissionCreated": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "submission": {
            "type": "object",
            "properties": {
              "sid": { "type": "string", "format": "uuid" },
              "title": { "type": "string" },
              "word_count": { "type": "integer" },
              "plan": { "type": "string" },
              "analysis_status": { "type": "string" },
              "status_url": { "type": "string", "format": "uri" }
            }
          },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      }
    }
  }
}
