{
  "openapi": "3.1.0",
  "info": {
    "title": "NexaPDF AI API",
    "version": "1.0.0",
    "summary": "Merge, convert, compress, sign, redact and read PDFs over HTTP.",
    "description": "The HTTP API behind [NexaPDF AI](https://www.nexapdfai.com) — 33 PDF tools plus an AI layer\n(summarize, translate, chat, PDF-to-Markdown).\n\n## How a tool run works\n\nEvery tool follows the same three steps:\n\n1. `POST /api/storage/presign-upload` with the file's content type, extension and byte size.\n   You get back `{ key, url }`.\n2. `PUT` the file bytes straight to `url` (S3 presigned URL). The API never sees the upload.\n3. `POST` the tool endpoint with `{ \"keys\": [key], \"options\": { ... } }`. You get back a\n   `downloadUrl` — a presigned URL for the result.\n\nLong-running tools (OCR, compress) can instead be submitted to `POST /api/jobs` and polled\nat `GET /api/jobs/{id}`.\n\n## Authentication\n\n**Optional.** Every tool here is usable without an account; anonymous callers are metered by\nIP. Sending a bearer token raises the ceilings and is required only for account-owned data\n(saved documents, subscriptions, signing history — not described here).\n\n## Rate limits\n\nResponses carry `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and\n`RateLimit-Policy`; a 429 additionally carries `Retry-After`. Self-throttle on those rather\nthan on a fixed delay.\n\n| Tier | Tool runs/min | /hour | /day |\n| --- | --- | --- | --- |\n| Guest (no token) | 20 | 200 | 500 |\n| Signed in | 60 | 1000 | 3000 |\n| Paid pass | 120 | 2000 | 6000 |\n\nPresign calls, job polling and config reads are counted in a separate, more generous\n\"plumbing\" bucket (60/min for guests) so browsing never spends a tool allowance.\n\n## Files\n\nUploads and results are removed by a one-day storage rule and are never used to train models.\n\n## Errors\n\nEvery error is JSON: `{ \"code\", \"message\", \"requestId\" }`. Branch on `code`, show `message`,\nquote `requestId` in a bug report.",
    "contact": {
      "name": "NexaPDF AI support",
      "url": "https://www.nexapdfai.com/contact",
      "email": "support@nexapdfai.com"
    },
    "termsOfService": "https://www.nexapdfai.com/terms",
    "license": {
      "name": "Proprietary",
      "url": "https://www.nexapdfai.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.nexapdfai.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://www.nexapdfai.com/docs"
  },
  "tags": [
    {
      "name": "Uploads",
      "description": "Getting file bytes into the pipeline."
    },
    {
      "name": "PDF tools",
      "description": "Synchronous PDF operations."
    },
    {
      "name": "AI",
      "description": "Model-backed document understanding. Metered in credits."
    },
    {
      "name": "Jobs",
      "description": "Asynchronous submission and polling."
    },
    {
      "name": "Account",
      "description": "Plan, quota and catalogue metadata."
    }
  ],
  "paths": {
    "/api/storage/presign-upload": {
      "post": {
        "tags": [
          "Uploads"
        ],
        "operationId": "presignUpload",
        "summary": "Get a presigned S3 URL to upload a file",
        "description": "Step 1 of every tool run. Validates the declared type and size against the caller's tier cap before any bytes move, so an over-size file is refused in a few hundred bytes rather than after a 100MB upload. `PUT` the file to the returned `url`, then name the returned `key` in the tool call.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresignUploadRequest"
              },
              "example": {
                "contentType": "application/pdf",
                "extension": "pdf",
                "sizeBytes": 482113,
                "tool": "merge-pdf"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A presigned upload URL and the object key to name in the tool call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PresignedUpload"
                },
                "example": {
                  "key": "uploads/6f1c0f2e-6a1f-4a1e-9a5d-2b7c8f0e1a33.pdf",
                  "url": "https://nexapdf-files-mum.s3.ap-south-1.amazonaws.com/uploads/..."
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body or option value.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign-in required for this tool or resource.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "A paid pass is required for this tool.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the caller's size cap.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unable to accept work. Honour `Retry-After`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pdf/{tool}": {
      "post": {
        "tags": [
          "PDF tools"
        ],
        "operationId": "runPdfTool",
        "summary": "Run a PDF tool on uploaded files",
        "description": "Step 3 of a tool run. `keys` are the object keys returned by presign-upload; `options` are tool-specific and all string-valued — for example `{\"ranges\": \"1-3,7\"}` for split-pdf, `{\"degrees\": \"90\"}` for rotate-pdf, `{\"password\": \"…\"}` for protect-pdf and unlock-pdf, `{\"level\": \"strong\"}` for compress-pdf. Most tools take exactly one key and refuse more than one rather than silently processing the first.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "tool",
            "in": "path",
            "required": true,
            "description": "Tool slug. Matches the URL of the tool's page on the website.",
            "schema": {
              "type": "string",
              "enum": [
                "merge-pdf",
                "split-pdf",
                "rotate-pdf",
                "watermark-pdf",
                "protect-pdf",
                "unlock-pdf",
                "jpg-to-pdf",
                "compress-pdf",
                "organize-pdf",
                "page-numbers",
                "crop-pdf",
                "repair-pdf",
                "pdf-to-jpg",
                "ocr-pdf",
                "scan-to-pdf",
                "compare-pdf",
                "redact-pdf",
                "pdf-forms",
                "pdf-to-pdfa",
                "sign-pdf",
                "edit-pdf",
                "word-to-pdf",
                "excel-to-pdf",
                "powerpoint-to-pdf",
                "html-to-pdf",
                "pdf-to-word",
                "pdf-to-excel",
                "pdf-to-powerpoint"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ToolRequest"
              },
              "example": {
                "keys": [
                  "uploads/6f1c0f2e-6a1f-4a1e-9a5d-2b7c8f0e1a33.pdf"
                ],
                "options": {
                  "ranges": "1-3,7"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ToolResult"
          },
          "400": {
            "description": "Invalid request body or option value.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign-in required for this tool or resource.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "A paid pass is required for this tool.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the caller's size cap.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unable to accept work. Honour `Retry-After`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "Processing started and was abandoned. Send a smaller file.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/ai/{operation}": {
      "post": {
        "tags": [
          "AI"
        ],
        "operationId": "runAiTool",
        "summary": "Run an AI operation on an uploaded document",
        "description": "Same upload-then-run shape as the PDF tools, metered additionally in AI credits. `summarize` accepts `{\"mode\": \"key-points\" | \"brief\" | \"abstract\"}`; `translate` takes `{\"targetLanguage\": \"es\"}` and returns a new PDF; `markdown` returns layout-aware Markdown for RAG pipelines; `chat` takes `{\"question\": \"…\"}` and answers from the document. A 503 `AI_UNAVAILABLE` means the model provider failed and the credit was refunded — retry is correct.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "operation",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "summarize",
                "translate",
                "markdown",
                "chat"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ToolRequest"
              },
              "example": {
                "keys": [
                  "uploads/6f1c0f2e-6a1f-4a1e-9a5d-2b7c8f0e1a33.pdf"
                ],
                "options": {
                  "mode": "key-points"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/ToolResult"
          },
          "400": {
            "description": "Invalid request body or option value.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign-in required for this tool or resource.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "A paid pass is required for this tool.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the caller's size cap.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unable to accept work. Honour `Retry-After`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "operationId": "submitJob",
        "summary": "Submit a long-running tool as an async job",
        "description": "For tools whose work outlives a sensible request timeout. Returns immediately with a job id; poll `GET /api/jobs/{id}` until `status` is `DONE` or `FAILED`. Send an `Idempotency-Key` header and a duplicate submit returns the SAME job rather than queueing (and charging for) the work twice.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobSubmitRequest"
              },
              "example": {
                "tool": "compress-pdf",
                "keys": [
                  "uploads/6f1c0f2e-6a1f-4a1e-9a5d-2b7c8f0e1a33.pdf"
                ],
                "options": {
                  "level": "strong"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted and queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobAccepted"
                },
                "example": {
                  "jobId": "0f0a5f0e-5a3e-4a2a-9d6c-2e6b7a1d4c55",
                  "status": "QUEUED"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body or option value.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Sign-in required for this tool or resource.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "A paid pass is required for this tool.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Upload exceeds the caller's size cap.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Temporarily unable to accept work. Honour `Retry-After`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/jobs/{id}": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "operationId": "getJob",
        "summary": "Poll an async job",
        "description": "Counted in the plumbing bucket, so polling never spends a tool allowance. A job submitted with a bearer token may only be polled with that same token.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current job state; `downloadUrl` appears once `status` is `DONE`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatus"
                },
                "example": {
                  "jobId": "0f0a5f0e-5a3e-4a2a-9d6c-2e6b7a1d4c55",
                  "status": "DONE",
                  "downloadUrl": "https://nexapdf-files-mum.s3.ap-south-1.amazonaws.com/results/...",
                  "resultBytes": 194233
                }
              }
            }
          },
          "400": {
            "description": "Malformed job id.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job, or it belongs to another account.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pricing": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getPricing",
        "summary": "The published plan ladder",
        "description": "Public. What each pass costs and what it unlocks.",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Plans and their limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/config/tools": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getToolConfig",
        "summary": "Which tools are enabled, and what each one requires",
        "description": "Public and authoritative: whether a tool needs an account or a paid pass is configuration, and this is where it is published. Read it rather than hard-coding a gate list.",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Per-tool availability and access requirements.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/subscription/quota": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getQuota",
        "summary": "Remaining AI credits for the caller",
        "description": "Public: a guest is metered too, and can read the allowance they are spending. Carries no payment or subscription detail.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Credit balance for the caller.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. `Retry-After` is the number of seconds to wait; the RateLimit-* headers describe the window that was exhausted.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actuator/health": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "health",
        "summary": "Service health",
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "`{\"status\":\"UP\"}` when the API and its dependencies are healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Optional. A Cognito ID token, sent as `Authorization: Bearer <token>`. Raises rate and size limits and unlocks account-owned resources. Every tool endpoint above also works with no token at all."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "An opaque, caller-generated string. Replaying a request with the same key returns the original result instead of doing (and charging for) the work again — which is what makes a retry after a network timeout safe.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "description": "Requests permitted in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests still permitted in the current window.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window has refilled enough for one more request.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Policy": {
        "description": "The policy the counters describe, e.g. `20;w=60`.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "ToolResult": {
        "description": "The tool ran. `downloadUrl` is a short-lived presigned URL for the result.",
        "headers": {
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimit-Limit"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimit-Remaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimit-Reset"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ToolResult"
            },
            "example": {
              "downloadUrl": "https://nexapdf-files-mum.s3.ap-south-1.amazonaws.com/results/...",
              "resultBytes": 194233
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "description": "Every failure from this API has this shape. `code` is stable and safe to branch on; `message` is written for a human and may change.",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable reason.",
            "enum": [
              "BAD_REQUEST",
              "PASSWORD_REJECTED",
              "SIGN_IN_REQUIRED",
              "FORBIDDEN",
              "OBJECT_FORBIDDEN",
              "NOT_FOUND",
              "METHOD_NOT_ALLOWED",
              "NOT_ACCEPTABLE",
              "PREMIUM_REQUIRED",
              "FILE_TOO_LARGE",
              "UNSUPPORTED_MEDIA_TYPE",
              "RATE_LIMITED",
              "AI_BUDGET_EXCEEDED",
              "SERVICE_BUSY",
              "AI_UNAVAILABLE",
              "PROCESSING_TIMEOUT",
              "PROCESSING_FAILED"
            ]
          },
          "message": {
            "type": "string",
            "description": "What went wrong, and where possible what to do about it."
          },
          "requestId": {
            "type": "string",
            "description": "Correlates this response with the server logs. Quote it in a bug report."
          },
          "field": {
            "type": "string",
            "description": "Present when one named input caused the failure, e.g. `password`."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "description": "Present on 429 — the same value as the `Retry-After` header."
          }
        },
        "example": {
          "code": "FILE_TOO_LARGE",
          "message": "That file is 42 MB. Guests can upload up to 10 MB — sign in for 25 MB.",
          "requestId": "524b70c3a73a4961a5a08b36da303705"
        }
      },
      "PresignUploadRequest": {
        "type": "object",
        "required": [
          "contentType",
          "extension",
          "sizeBytes"
        ],
        "properties": {
          "contentType": {
            "type": "string",
            "description": "MIME type of the file, e.g. `application/pdf`."
          },
          "extension": {
            "type": "string",
            "description": "File extension without the dot, e.g. `pdf`."
          },
          "sizeBytes": {
            "type": "integer",
            "minimum": 1,
            "description": "Exact byte length of the upload."
          },
          "tool": {
            "type": "string",
            "description": "The tool this upload is for. Optional, and worth sending: it moves an over-size rejection ahead of the upload instead of after it."
          }
        }
      },
      "PresignedUpload": {
        "type": "object",
        "required": [
          "key",
          "url"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Object key to name in `keys` on the tool call."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Short-lived presigned URL to `PUT` the bytes to."
          }
        }
      },
      "ToolRequest": {
        "type": "object",
        "required": [
          "keys"
        ],
        "properties": {
          "keys": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Object keys from presign-upload, in the order the tool should read them."
          },
          "options": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Tool-specific options. All values are strings."
          }
        }
      },
      "ToolResult": {
        "type": "object",
        "required": [
          "downloadUrl",
          "resultBytes"
        ],
        "properties": {
          "downloadUrl": {
            "type": "string",
            "format": "uri",
            "description": "Presigned URL for the produced file."
          },
          "resultBytes": {
            "type": "integer",
            "description": "Size of the result in bytes."
          },
          "compression": {
            "type": "object",
            "additionalProperties": true,
            "description": "Compress PDF only — what it did to the file, or why it could not shrink it."
          }
        }
      },
      "JobSubmitRequest": {
        "type": "object",
        "required": [
          "tool",
          "keys"
        ],
        "properties": {
          "tool": {
            "type": "string",
            "description": "Tool slug. Only tools that run asynchronously are accepted."
          },
          "keys": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "options": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "JobAccepted": {
        "type": "object",
        "required": [
          "jobId",
          "status"
        ],
        "properties": {
          "jobId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "QUEUED",
              "RUNNING",
              "DONE",
              "FAILED"
            ]
          }
        }
      },
      "JobStatus": {
        "type": "object",
        "required": [
          "jobId",
          "status"
        ],
        "properties": {
          "jobId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "QUEUED",
              "RUNNING",
              "DONE",
              "FAILED"
            ]
          },
          "downloadUrl": {
            "type": "string",
            "format": "uri",
            "description": "Present once `status` is `DONE`."
          },
          "resultBytes": {
            "type": "integer"
          },
          "error": {
            "type": "string",
            "description": "Present when `status` is `FAILED`."
          }
        }
      }
    }
  }
}
