{
  "openapi": "3.1.0",
  "info": {
    "title": "PDF to Markdown (AI actions)",
    "version": "2.0.0",
    "description": "Reduced action spec for AI clients and ChatGPT Custom GPT Actions. Convert a PDF to clean Markdown: create a job, poll status, download the Markdown. Convenience subset of the full API at /api/v2/openapi.json (not a security boundary): the same bearer API-key auth, scopes and per-tier limits apply. Authenticate with Authorization: Bearer p2m_... Never claim a result before status=ready; if truncated=true the document was returned partially up to the tier time budget."
  },
  "servers": [
    { "url": "https://pdf2md.dev", "description": "Production (primary)" }
  ],
  "security": [ { "ApiKey": [] } ],
  "paths": {
    "/api/v2/jobs": {
      "post": {
        "operationId": "createJobFromUrl",
        "summary": "Create a conversion job from a PDF URL",
        "description": "Submits a PDF URL for conversion to Markdown. Returns a job with status=queued. Requires the jobs:create scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateJobRequest" }
            }
          }
        },
        "responses": {
          "201": { "description": "Job created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "400": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "No free slot; delete a finished job or wait", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "413": { "description": "File too large for your tier", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limited; honor the Retry-After header", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v2/jobs/{jobId}": {
      "get": {
        "operationId": "getJob",
        "summary": "Get job status",
        "description": "Returns the current job, including status, pages and truncated. Requires the jobs:read scope.",
        "parameters": [ { "$ref": "#/components/parameters/JobId" } ],
        "responses": {
          "200": { "description": "Job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Job" } } } },
          "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Unknown job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v2/jobs/{jobId}/download": {
      "get": {
        "operationId": "getJobMarkdown",
        "summary": "Download the converted Markdown",
        "description": "Returns the Markdown once status=ready. Requires the jobs:download scope. Check the job's truncated flag first.",
        "parameters": [ { "$ref": "#/components/parameters/JobId" } ],
        "responses": {
          "200": { "description": "Markdown", "content": { "text/markdown": { "schema": { "type": "string" } } } },
          "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Unknown job", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Not ready yet", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "Result expired (past retention window)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v2/limits": {
      "get": {
        "operationId": "getLimits",
        "summary": "Get the caller's tier limits and current slot usage",
        "responses": {
          "200": { "description": "Limits", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Limits" } } } },
          "401": { "description": "Missing or invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "description": "API key over TLS: Authorization: Bearer p2m_..."
      }
    },
    "parameters": {
      "JobId": {
        "name": "jobId",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "The job id returned by createJobFromUrl."
      }
    },
    "schemas": {
      "CreateJobRequest": {
        "type": "object",
        "required": [ "url" ],
        "properties": {
          "url": { "type": "string", "format": "uri", "description": "Direct URL to a PDF to convert." },
          "file_name": { "type": "string", "description": "Optional name for the output." },
          "external_id": { "type": "string", "description": "Optional caller-supplied id echoed back on the job." },
          "callback_url": { "type": "string", "format": "uri", "description": "Optional webhook URL to notify on completion (paid tiers)." }
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "job_id": { "type": "string" },
          "status": { "type": "string", "enum": [ "queued", "processing", "ready", "error", "canceled", "deleted" ] },
          "file_name": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "status_since": { "type": "string", "format": "date-time" },
          "pages": { "type": "integer", "description": "Page count once known." },
          "truncated": { "type": "boolean", "description": "True if the document was returned partially up to the tier time budget." },
          "output_size": { "type": "integer", "description": "Size of the Markdown output in bytes." },
          "download_url": { "type": "string", "description": "Relative path to download the Markdown once ready." },
          "tier": { "type": "string" },
          "slot_usage": { "$ref": "#/components/schemas/SlotUsage" },
          "external_id": { "type": "string" }
        }
      },
      "SlotUsage": {
        "type": "object",
        "properties": {
          "used": { "type": "integer" },
          "limit": { "type": "integer" }
        }
      },
      "Limits": {
        "type": "object",
        "properties": {
          "tier": { "type": "string" },
          "max_active_slots": { "type": "integer" },
          "max_file_size_bytes": { "type": "integer" },
          "processing_soft_budget_sec": { "type": "integer", "description": "Time budget per document; output past this is truncated." },
          "ready_ttl_sec": { "type": "integer", "description": "Retention window for a ready result, in seconds." },
          "webhooks_enabled": { "type": "boolean" },
          "slots_in_use": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "description": "Stable error code, e.g. validation_error, rate_limited, not_ready, slots_full, gone." },
              "message": { "type": "string" }
            }
          }
        }
      }
    }
  }
}
