openapi: 3.0.3
info:
  title: PDFStack API
  version: 1.2.0
  description: |
    Customer-facing API for PDF processing and rendering.

    Authentication flow:
    1) POST /auth/signup
    2) Verify email
    3) POST /auth/login
    4) POST /auth/create-key
    5) Use X-API-Key for processing endpoints
servers:
  - url: https://pdfstack.dev/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development

tags:
  - name: Auth
  - name: PDF
  - name: Rendering
  - name: Billing
  - name: Admin

paths:
  /auth/signup:
    post:
      tags: [Auth]
      summary: Create account
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  minLength: 6
      responses:
        '200': { description: Signup successful }
        '400': { $ref: '#/components/responses/BadRequest' }
        '500': { $ref: '#/components/responses/InternalServerError' }

  /auth/login:
    post:
      tags: [Auth]
      summary: Login and return bearer session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
      responses:
        '200': { description: Login successful }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current user and API key status
      security:
        - BearerAuth: []
      responses:
        '200': { description: Current user details }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /auth/create-key:
    post:
      tags: [Auth]
      summary: Create first API key for authenticated user
      security:
        - BearerAuth: []
      responses:
        '200': { description: API key created or existing key returned }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /auth/regenerate-key:
    post:
      tags: [Auth]
      summary: Regenerate and rotate API key
      security:
        - BearerAuth: []
      responses:
        '200': { description: API key regenerated }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /auth/revoke-key:
    delete:
      tags: [Auth]
      summary: Revoke current API key
      security:
        - BearerAuth: []
      responses:
        '200': { description: API key revoked }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /auth/my-keys:
    get:
      tags: [Auth]
      summary: List keys for authenticated user
      security:
        - BearerAuth: []
      responses:
        '200': { description: API key list }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /auth/verify:
    get:
      tags: [Auth]
      summary: Verify API key validity
      parameters:
        - in: query
          name: apiKey
          required: true
          schema: { type: string }
      responses:
        '200': { description: Key validity result }
        '400': { $ref: '#/components/responses/BadRequest' }

  /pdf/convert:
    post:
      tags: [PDF]
      summary: Convert PDF to text or docx
      description: |
        Extracts text layer from PDF and can generate DOCX output.

        **Limitations:**
        - Scanned PDFs (no text layer) are not supported — OCR not implemented
        - Encrypted/password-protected PDFs are not supported
        - Text extraction quality depends on PDF structure
        - DOCX conversion preserves basic formatting only (no complex layouts)
        - Maximum file size: 10MB
        - Processing timeout: 30 seconds
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://pdfstack.dev/api/v1/pdf/convert" \
              -H "X-API-Key: YOUR_API_KEY" \
              -F "file=@document.pdf" \
              -F "format=text"
        - lang: JavaScript
          source: |
            const form = new FormData();
            form.append("file", fileBlob, "document.pdf");
            form.append("format", "text");
            const res = await fetch("https://pdfstack.dev/api/v1/pdf/convert", {
              method: "POST",
              headers: { "X-API-Key": process.env.PDFSTACK_API_KEY },
              body: form
            });
            const payload = await res.json();
            if (!res.ok) throw new Error(payload?.error?.code || "convert failed");
        - lang: Python
          source: |
            r = requests.post(
              "https://pdfstack.dev/api/v1/pdf/convert",
              headers={"X-API-Key": os.environ["PDFSTACK_API_KEY"]},
              files={"file": ("document.pdf", open("document.pdf", "rb"), "application/pdf")},
              data={"format": "text"},
              timeout=60,
            )
            payload = r.json()
            if not r.ok: raise RuntimeError(payload.get("error", {}).get("code", "convert failed"))
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                format:
                  type: string
                  enum: [text, docx, word]
                  default: text
                delivery:
                  type: string
                  enum: [inline, url, auto]
                  default: inline
                  description: Delivery mode for output file payload. Default is inline when omitted.
      responses:
        '200': { description: Conversion succeeded }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '413': { $ref: '#/components/responses/PayloadTooLarge' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /pdf/merge:
    post:
      tags: [PDF]
      summary: Merge 2-10 PDFs
      description: |
        Combine multiple PDF files into a single document.

        **Limitations:**
        - Minimum 2 files, maximum 10 files per request
        - All files must be valid PDFs (no mixed formats)
        - Encrypted/password-protected PDFs are not supported
        - Total combined size limit: 25MB
        - Only metadata from first PDF is preserved
        - Processing timeout: 30 seconds
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://pdfstack.dev/api/v1/pdf/merge" \
              -H "X-API-Key: YOUR_API_KEY" \
              -F "files=@a.pdf" \
              -F "files=@b.pdf"
        - lang: JavaScript
          source: |
            const form = new FormData();
            form.append("files", aBlob, "a.pdf");
            form.append("files", bBlob, "b.pdf");
            const res = await fetch("https://pdfstack.dev/api/v1/pdf/merge", {
              method: "POST",
              headers: { "X-API-Key": process.env.PDFSTACK_API_KEY },
              body: form
            });
            const payload = await res.json();
            if (!res.ok) throw new Error(payload?.error?.code || "merge failed");
        - lang: Python
          source: |
            files = [("files", ("a.pdf", open("a.pdf", "rb"), "application/pdf")), ("files", ("b.pdf", open("b.pdf", "rb"), "application/pdf"))]
            r = requests.post("https://pdfstack.dev/api/v1/pdf/merge", headers={"X-API-Key": os.environ["PDFSTACK_API_KEY"]}, files=files, timeout=60)
            payload = r.json()
            if not r.ok: raise RuntimeError(payload.get("error", {}).get("code", "merge failed"))
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                delivery:
                  type: string
                  enum: [inline, url, auto]
                  default: inline
                  description: Delivery mode for output file payload. Default is inline when omitted.
      responses:
        '200': { description: Merge succeeded }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /pdf/split:
    post:
      tags: [PDF]
      summary: Split PDF by page numbers
      description: |
        Extract specific pages from a PDF document by page number.

        **Limitations:**
        - Single PDF input only (use merge for multi-file operations)
        - Page ranges must be valid format (e.g., "1,3,5" or "1-5")
        - Cannot split encrypted/password-protected PDFs
        - Maximum input size: 10MB
        - Processing timeout: 30 seconds
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://pdfstack.dev/api/v1/pdf/split" \
              -H "X-API-Key: YOUR_API_KEY" \
              -F "file=@document.pdf" \
              -F "pages=1,3,5"
        - lang: JavaScript
          source: |
            const form = new FormData();
            form.append("file", fileBlob, "document.pdf");
            form.append("pages", "1,3,5");
            const res = await fetch("https://pdfstack.dev/api/v1/pdf/split", {
              method: "POST",
              headers: { "X-API-Key": process.env.PDFSTACK_API_KEY },
              body: form
            });
            const payload = await res.json();
            if (!res.ok) throw new Error(payload?.error?.code || "split failed");
        - lang: Python
          source: |
            r = requests.post(
              "https://pdfstack.dev/api/v1/pdf/split",
              headers={"X-API-Key": os.environ["PDFSTACK_API_KEY"]},
              files={"file": ("document.pdf", open("document.pdf", "rb"), "application/pdf")},
              data={"pages": "1,3,5"},
              timeout=60,
            )
            payload = r.json()
            if not r.ok: raise RuntimeError(payload.get("error", {}).get("code", "split failed"))
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, pages]
              properties:
                file:
                  type: string
                  format: binary
                pages:
                  type: string
                  example: '1,3,5'
                delivery:
                  type: string
                  enum: [inline, url, auto]
                  default: inline
                  description: Delivery mode for split PDF outputs. Default is inline when omitted.
      responses:
        '200': { description: Split succeeded }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /screenshot:
    post:
      tags: [Rendering]
      summary: Capture URL or HTML as png/jpeg/pdf
      description: |
        Capture webpage screenshots as PNG, JPEG, or PDF from a URL or raw HTML.

        **Limitations:**
        - JavaScript-heavy sites may require longer timeout or may not render correctly
        - Some sites detect and block headless browser access
        - Login/authentication flows not supported (use pre-authenticated URLs)
        - Maximum viewport: 3840×2160 (4K)
        - Navigation timeout: 30 seconds default (configurable up to 60s)
        - Large full-page screenshots may exceed 4MB response limit
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://pdfstack.dev/api/v1/screenshot" \
              -H "X-API-Key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{"url":"https://example.com","format":"png","fullPage":true}'
        - lang: JavaScript
          source: |
            const res = await fetch("https://pdfstack.dev/api/v1/screenshot", {
              method: "POST",
              headers: { "X-API-Key": process.env.PDFSTACK_API_KEY, "Content-Type": "application/json" },
              body: JSON.stringify({ url: "https://example.com", format: "png", fullPage: true })
            });
            const payload = await res.json();
            if (!res.ok) throw new Error(payload?.error?.code || "screenshot failed");
        - lang: Python
          source: |
            r = requests.post(
              "https://pdfstack.dev/api/v1/screenshot",
              headers={"X-API-Key": os.environ["PDFSTACK_API_KEY"], "Content-Type": "application/json"},
              json={"url": "https://example.com", "format": "png", "fullPage": True},
              timeout=60,
            )
            payload = r.json()
            if not r.ok: raise RuntimeError(payload.get("error", {}).get("code", "screenshot failed"))
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url:
                  type: string
                  description: Website URL to capture (required if html not provided)
                html:
                  type: string
                  description: Raw HTML to render (required if url not provided)
                format:
                  type: string
                  enum: [png, jpeg, pdf]
                  default: png
                  description: Output format
                viewport:
                  type: object
                  properties:
                    width:
                      type: number
                      default: 1280
                    height:
                      type: number
                      default: 720
                  description: Viewport dimensions (max 3840×2160)
                fullPage:
                  type: boolean
                  default: false
                  description: Capture full scrollable page
                waitFor:
                  type: string
                  description: CSS selector to wait for before capturing
                quality:
                  type: number
                  default: 80
                  minimum: 0
                  maximum: 100
                  description: Image quality (JPEG only)
                clip:
                  type: object
                  properties:
                    x: { type: number }
                    y: { type: number }
                    width: { type: number }
                    height: { type: number }
                  description: Capture specific region (overrides fullPage)
                omitBackground:
                  type: boolean
                  default: false
                  description: Transparent background (PNG only)
                timeout:
                  type: number
                  default: 30000
                  maximum: 60000
                  description: Navigation timeout in milliseconds
                waitUntil:
                  type: string
                  enum: [load, domcontentloaded, networkidle0, networkidle2]
                  default: networkidle0
                  description: When to consider navigation done
                captureBeyondViewport:
                  type: boolean
                  default: true
                  description: Capture content outside viewport when fullPage=true
                delivery:
                  type: string
                  enum: [inline, url, auto]
                  default: inline
                  description: Delivery mode for screenshot payload. Default is inline when omitted.
      responses:
        '200': { description: Capture succeeded }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /html-to-pdf:
    post:
      tags: [Rendering]
      summary: Render HTML or URL to PDF
      description: |
        Convert HTML content or a URL to a PDF document.

        **Limitations:**
        - Complex CSS layouts (flexbox, grid) may not render perfectly
        - External resources (images, fonts) must be accessible (CORS restrictions)
        - JavaScript execution limited to page load (no async operations)
        - Maximum HTML input: 1MB
        - Navigation timeout: 30 seconds default (configurable up to 60s)
        - Large HTML may exceed 4MB response limit
      security:
        - ApiKeyAuth: []
      x-codeSamples:
        - lang: curl
          source: |
            curl -X POST "https://pdfstack.dev/api/v1/html-to-pdf" \
              -H "X-API-Key: YOUR_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{"html":"<h1>Hello PDFStack</h1>","options":{"format":"A4","printBackground":true}}'
        - lang: JavaScript
          source: |
            const res = await fetch("https://pdfstack.dev/api/v1/html-to-pdf", {
              method: "POST",
              headers: { "X-API-Key": process.env.PDFSTACK_API_KEY, "Content-Type": "application/json" },
              body: JSON.stringify({ html: "<h1>Hello PDFStack</h1>", options: { format: "A4", printBackground: true } })
            });
            const payload = await res.json();
            if (!res.ok) throw new Error(payload?.error?.code || "html-to-pdf failed");
        - lang: Python
          source: |
            r = requests.post(
              "https://pdfstack.dev/api/v1/html-to-pdf",
              headers={"X-API-Key": os.environ["PDFSTACK_API_KEY"], "Content-Type": "application/json"},
              json={"html": "<h1>Hello PDFStack</h1>", "options": {"format": "A4", "printBackground": True}},
              timeout=60,
            )
            payload = r.json()
            if not r.ok: raise RuntimeError(payload.get("error", {}).get("code", "html-to-pdf failed"))
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                html:
                  type: string
                  description: Raw HTML content to convert (required if url not provided)
                url:
                  type: string
                  description: URL to convert to PDF (required if html not provided)
                options:
                  type: object
                  properties:
                    format:
                      type: string
                      enum: [A4, Letter, Legal]
                      default: A4
                      description: Paper size
                    landscape:
                      type: boolean
                      default: false
                      description: Page orientation
                    printBackground:
                      type: boolean
                      default: true
                      description: Include background graphics
                    margin:
                      type: object
                      properties:
                        top: { type: string, default: "20px" }
                        right: { type: string, default: "20px" }
                        bottom: { type: string, default: "20px" }
                        left: { type: string, default: "20px" }
                      description: Page margins (CSS units)
                    scale:
                      type: number
                      default: 1.0
                      minimum: 0.1
                      maximum: 2.0
                      description: Zoom factor for webpage rendering
                    displayHeaderFooter:
                      type: boolean
                      default: false
                      description: Display header and footer on each page
                    headerTemplate:
                      type: string
                      default: ''
                      description: HTML template for page header
                    footerTemplate:
                      type: string
                      default: ''
                      description: HTML template for page footer
                    pageRanges:
                      type: string
                      default: ''
                      description: Paper ranges to print (e.g., "1-5, 8, 11-13")
                    preferCSSPageSize:
                      type: boolean
                      default: false
                      description: Give CSS @page size priority over format
                    omitBackground:
                      type: boolean
                      default: false
                      description: Transparent background
                    timeout:
                      type: number
                      default: 30000
                      maximum: 60000
                      description: Navigation timeout in milliseconds
                    waitUntil:
                      type: string
                      enum: [load, domcontentloaded, networkidle0, networkidle2]
                      default: networkidle0
                      description: When to consider navigation done
                delivery:
                  type: string
                  enum: [inline, url, auto]
                  default: inline
                  description: Delivery mode for rendered PDF. Default is inline when omitted.
      responses:
        '200': { description: PDF render succeeded }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/TooManyRequests' }

  /pdf/compress:
    post:
      tags: [PDF]
      summary: Compress base64 PDF
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200': { description: Compression succeeded }

  /pdf/to-image:
    post:
      tags: [PDF]
      summary: Convert base64 PDF to images
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200': { description: Conversion succeeded }

  /pdf/from-image:
    post:
      tags: [PDF]
      summary: Convert base64 images to PDF
      security: [{ ApiKeyAuth: [] }]
      responses:
        '200': { description: Conversion succeeded }

  /billing/plans:
    get:
      tags: [Billing]
      summary: List subscription plans
      responses:
        '200': { description: Plans returned }

  /billing/status:
    get:
      tags: [Billing]
      summary: Usage and plan status for API key
      parameters:
        - in: query
          name: apiKey
          required: true
          schema: { type: string }
      responses:
        '200': { description: Status returned }

  /billing/usage-breakdown:
    get:
      tags: [Billing]
      summary: Endpoint usage breakdown for API key
      parameters:
        - in: query
          name: apiKey
          required: true
          schema: { type: string }
      responses:
        '200': { description: Breakdown returned }

  /billing/create-checkout-session:
    post:
      tags: [Billing]
      summary: Create Stripe checkout session
      security: [{ BearerAuth: [] }]
      responses:
        '200': { description: Checkout session created }

  /billing/create-portal-session:
    post:
      tags: [Billing]
      summary: Create Stripe customer portal session
      security: [{ BearerAuth: [] }]
      responses:
        '200': { description: Portal session created }

  /admin/analytics/funnel:
    get:
      tags: [Admin]
      summary: Admin conversion funnel analytics
      security: [{ BearerAuth: [] }]
      parameters:
        - in: query
          name: from
          required: false
          schema: { type: string, format: date-time }
        - in: query
          name: to
          required: false
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Funnel metrics returned }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /admin/analytics/activation:
    get:
      tags: [Admin]
      summary: Admin activation analytics
      security: [{ BearerAuth: [] }]
      parameters:
        - in: query
          name: from
          required: false
          schema: { type: string, format: date-time }
        - in: query
          name: to
          required: false
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Activation metrics returned }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /admin/analytics/health:
    get:
      tags: [Admin]
      summary: Admin operational health analytics
      security: [{ BearerAuth: [] }]
      parameters:
        - in: query
          name: from
          required: false
          schema: { type: string, format: date-time }
        - in: query
          name: to
          required: false
          schema: { type: string, format: date-time }
      responses:
        '200': { description: Health metrics returned }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  schemas:
    ErrorEnvelope:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          properties:
            message: { type: string }
            code: { type: string }
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Unauthorized:
      description: Missing/invalid authentication
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Forbidden:
      description: Authenticated but not allowed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    PayloadTooLarge:
      description: File too large for plan
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    TooManyRequests:
      description: Usage limit exceeded
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT


