{
  "openapi": "3.0.3",
  "info": {
    "title": "conv2pdf API",
    "version": "1.0.0",
    "description": "API REST de conversion et de manipulation de PDF, hébergée en France (OVH Gravelines), conforme RGPD, DPA fourni. Aucun service tiers hors UE n'intervient dans le traitement. Les fichiers d'entrée et de sortie sont supprimés au bout d'une heure, aucun résultat n'est mis en cache.\n\nAuthentification par clé API (`Authorization: Bearer cpdf_live_...`), à créer depuis le tableau de bord. Un même endpoint `POST /convert/{tool}` expose les 14 outils ; le format de sortie dépend de l'outil.",
    "termsOfService": "https://conv2pdf.com/legal/cgv/",
    "contact": {
      "name": "conv2pdf",
      "url": "https://conv2pdf.com/contact/",
      "email": "contact@conv2pdf.com"
    }
  },
  "servers": [
    { "url": "https://api.conv2pdf.com/v1", "description": "Production" }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Conversion", "description": "Conversion et manipulation de fichiers" },
    { "name": "Jobs", "description": "Récupération, statut et suppression des jobs" }
  ],
  "paths": {
    "/tools": {
      "get": {
        "tags": ["Conversion"],
        "summary": "Liste des outils disponibles",
        "description": "Renvoie la liste des 14 outils, leurs bornes de fichiers et extensions acceptées. Source machine de référence pour les intégrations.",
        "operationId": "listTools",
        "responses": {
          "200": {
            "description": "Liste des outils",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ToolsList" }
              }
            }
          }
        }
      }
    },
    "/convert/{tool}": {
      "post": {
        "tags": ["Conversion"],
        "summary": "Convertir ou manipuler un fichier",
        "description": "Effectue une conversion synchrone. Le fichier est envoyé en `multipart/form-data`. La réponse est un JSON contenant l'URL de téléchargement du résultat (récupérable via `GET /download/{jobId}` pendant 1 heure).\n\nCertains outils acceptent des champs de formulaire supplémentaires (voir la propriété `file` et les champs optionnels ci-dessous). `merge-pdf` accepte le champ `file` répété (2 à 20 fichiers).",
        "operationId": "convert",
        "parameters": [
          {
            "name": "tool",
            "in": "path",
            "required": true,
            "description": "Identifiant de l'outil.",
            "schema": {
              "type": "string",
              "enum": [
                "image-to-pdf",
                "heic-to-jpg",
                "heic-to-pdf",
                "office-to-pdf",
                "merge-pdf",
                "split-pdf",
                "compress-pdf",
                "protect-pdf",
                "pdf-to-word",
                "pdf-to-image",
                "rotate-pdf",
                "unlock-pdf",
                "watermark-pdf",
                "page-numbers-pdf"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["file"],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Fichier d'entrée. Extensions acceptées selon l'outil (cf. `GET /tools`). Taille max 200 Mo par clé API. Pour `merge-pdf`, répéter le champ `file` (2 à 20 fichiers)."
                  },
                  "ranges": {
                    "type": "string",
                    "description": "`split-pdf` (requis) : pages à extraire, ex. `1-5,7,10-12`.",
                    "example": "1-5,7,10-12"
                  },
                  "quality": {
                    "type": "string",
                    "enum": ["low", "medium", "high"],
                    "default": "medium",
                    "description": "`compress-pdf` : niveau de compression (low = 72 DPI, medium = 150 DPI, high = 300 DPI)."
                  },
                  "password": {
                    "type": "string",
                    "description": "`protect-pdf` (requis, 4 à 64 caractères) : mot de passe à poser. `unlock-pdf` (requis) : mot de passe actuel du PDF, à retirer."
                  },
                  "prevent_print": {
                    "type": "string",
                    "enum": ["on"],
                    "description": "`protect-pdf` (optionnel) : interdit l'impression."
                  },
                  "prevent_copy": {
                    "type": "string",
                    "enum": ["on"],
                    "description": "`protect-pdf` (optionnel) : interdit la copie de texte."
                  },
                  "rotation": {
                    "type": "string",
                    "enum": ["90", "180", "270"],
                    "description": "`rotate-pdf` (requis) : angle de rotation en degrés (horaire)."
                  },
                  "text": {
                    "type": "string",
                    "description": "`watermark-pdf` (requis, max 50 caractères) : texte du filigrane.",
                    "example": "CONFIDENTIEL"
                  },
                  "format": {
                    "type": "string",
                    "description": "`pdf-to-image` : `png` (défaut) ou `jpg`. `page-numbers-pdf` : `full` (défaut, « X sur N ») ou `simple` (« X » seul)."
                  },
                  "position": {
                    "type": "string",
                    "enum": ["bottom-center", "bottom-left", "bottom-right"],
                    "default": "bottom-center",
                    "description": "`page-numbers-pdf` (optionnel) : position du numéro."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Conversion réussie",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ConvertSuccess" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PlanLimit" },
          "404": { "$ref": "#/components/responses/ToolNotFound" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "415": { "$ref": "#/components/responses/UnsupportedMedia" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/QuotaExceeded" },
          "500": { "$ref": "#/components/responses/ServerError" },
          "503": { "$ref": "#/components/responses/ServerBusy" }
        }
      }
    },
    "/download/{jobId}": {
      "get": {
        "tags": ["Jobs"],
        "summary": "Télécharger le fichier converti",
        "description": "Renvoie le fichier de sortie avec son type MIME (`application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document` pour PDF → Word, `image/jpeg` pour HEIC → JPG, `application/zip` pour PDF → Image) et `Cache-Control: no-store`. Disponible 1 heure après la conversion.",
        "operationId": "download",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Fichier binaire",
            "content": {
              "application/octet-stream": {
                "schema": { "type": "string", "format": "binary" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/JobNotFound" },
          "409": { "$ref": "#/components/responses/JobNotReady" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      }
    },
    "/job/{jobId}": {
      "get": {
        "tags": ["Jobs"],
        "summary": "Statut et métadonnées d'un job",
        "description": "Renvoie le statut d'un job (utile pour vérifier qu'une conversion est prête avant de télécharger).",
        "operationId": "getJob",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Statut du job",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobStatus" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/JobNotFound" },
          "409": { "$ref": "#/components/responses/JobNotReady" },
          "410": { "$ref": "#/components/responses/Gone" }
        }
      },
      "delete": {
        "tags": ["Jobs"],
        "summary": "Supprimer un job",
        "description": "Supprime immédiatement un job et son fichier, sans attendre l'expiration automatique (1 heure).",
        "operationId": "deleteJob",
        "parameters": [{ "$ref": "#/components/parameters/JobId" }],
        "responses": {
          "200": {
            "description": "Job supprimé",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "status": { "type": "string", "example": "deleted" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/JobNotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Clé API au format `cpdf_live_...`, créée depuis le tableau de bord. En-tête `Authorization: Bearer cpdf_live_...`."
      }
    },
    "parameters": {
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "description": "Identifiant du job renvoyé par `POST /convert/{tool}`.",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "ToolsList": {
        "type": "object",
        "properties": {
          "tools": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string", "example": "pdf-to-word" },
                "min_files": { "type": "integer", "example": 1 },
                "max_files": { "type": "integer", "example": 1 },
                "accepted_exts": {
                  "type": "array",
                  "items": { "type": "string" },
                  "example": [".pdf"]
                }
              }
            }
          }
        }
      },
      "ConvertSuccess": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string", "example": "abc123def456" },
          "status": { "type": "string", "enum": ["success"] },
          "download_url": { "type": "string", "example": "/v1/download/abc123def456" },
          "size_bytes": { "type": "integer", "example": 124533 },
          "quota": {
            "$ref": "#/components/schemas/Quota",
            "description": "Présent uniquement pour les appels authentifiés par clé API."
          }
        }
      },
      "Quota": {
        "type": "object",
        "description": "État du quota du compte. Le quota est mensuel sur tous les plans, sur un cycle anniversaire : `quota_period_end` = timestamp de la prochaine remise à zéro, c'est la seule valeur sur laquelle caler un back-off. `period_end` est la borne de FACTURATION, identique à `quota_period_end` en mensuel mais située jusqu'à douze mois plus loin en annuel. Un dépassement de 10 % est toléré (`soft_cap_limit`) avant blocage en 429.",
        "properties": {
          "plan": { "type": "string", "example": "starter" },
          "quota": { "type": "integer", "example": 1000 },
          "used": { "type": "integer", "example": 42 },
          "soft_cap_limit": { "type": "integer", "example": 1100 },
          "status": { "type": "string", "enum": ["ok", "soft_cap", "hard_cap"] },
          "period_end": { "type": "integer", "format": "int64", "example": 1715789012345 },
          "quota_period_end": { "type": "integer", "format": "int64", "example": 1715789012345 }
        }
      },
      "JobStatus": {
        "type": "object",
        "description": "Renvoyé uniquement pour un job terminé avec succès (sinon 409 job_not_ready).",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": ["success"] },
          "tool": { "type": "string", "example": "pdf-to-word" },
          "size_bytes": { "type": "integer", "example": 124533 },
          "completed_at": { "type": "integer", "format": "int64" },
          "expires_at": { "type": "integer", "format": "int64", "description": "Timestamp de suppression automatique du fichier." },
          "download_url": { "type": "string", "example": "/v1/download/abc123" },
          "can_share": { "type": "boolean" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string", "description": "Code d'erreur stable, lisible par machine." }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Nombre de fichiers hors bornes (`not_enough_files`, `too_many_files`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Auth manquante ou clé invalide (`missing_bearer_token`, `invalid_api_key`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PlanLimit": {
        "description": "Limite du plan atteinte, ex. nombre de fichiers (`plan_limit_files`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "Le job appartient à une autre clé (`forbidden`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ToolNotFound": {
        "description": "Outil inexistant (`tool_not_found`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "JobNotFound": {
        "description": "Job inexistant (`job_not_found`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "JobNotReady": {
        "description": "Conversion non terminée (`job_not_ready`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Gone": {
        "description": "Fichier supprimé, TTL de 1 h dépassé (`file_expired`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PayloadTooLarge": {
        "description": "Fichier supérieur à 200 Mo (`file_too_large`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UnsupportedMedia": {
        "description": "Extension non acceptée par l'outil, ou contenu réel incompatible (`unsupported_format`, `unsupported_content`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UnprocessableEntity": {
        "description": "Fichier vide (`empty_file`), PDF chiffré (`password_protected`), PDF scanné sans couche texte pour PDF → Word (`pdf_scanned_needs_ocr`), trop de pages (`pdf_too_many_pages`, `too_many_pages`), plage invalide (`invalid_page_range`), rotation invalide (`invalid_rotation`), sortie trop volumineuse (`output_too_large`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "QuotaExceeded": {
        "description": "Quota mensuel atteint (`quota_exceeded`). Voir `soft_cap_limit` et `quota_period_end` (date du prochain rechargement) dans l'objet quota.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServerBusy": {
        "description": "File de conversion saturée, réessayer sous 5 s (`server_busy`, en-tête `Retry-After`).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ServerError": {
        "description": "Erreur de conversion serveur (`failed`). La tentative n'est pas décomptée du quota.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
