openapi: 3.1.0
info:
  title: PDF Generation API
  version: '1.0.0'
  description: >
    Convert HTML, a URL, or Markdown into a PDF. Agent-first: structured errors,
    idempotent retries, and a machine-readable manifest at /v1/manifest.
  contact:
    name: Support
    url: https://gumroad.com/
servers:
  - url: https://quill.rgtz.dev
    description: Production
  - url: https://your-api-id.execute-api.us-east-1.amazonaws.com
    description: Direct API Gateway URL (fallback)
security:
  - bearerAuth: []
paths:
  /v1/pdf:
    post:
      operationId: generatePdf
      summary: Generate a PDF from html, url, or markdown.
      description: Provide exactly one of html, url, or markdown.
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
          description: Retry-safe key. Repeats return the original result without re-billing.
        - $ref: '#/components/parameters/XRequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateRequest'
            examples:
              markdown:
                value:
                  markdown: "# Invoice 1024\n\n**Total:** $42.00"
                  options:
                    format: A4
              url:
                value:
                  url: https://example.com
                  options:
                    landscape: true
      responses:
        '200':
          description: PDF generated.
          headers:
            x-request-id: { $ref: '#/components/headers/XRequestId' }
            x-ratelimit-limit: { $ref: '#/components/headers/XRateLimitLimit' }
            x-ratelimit-remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
            x-ratelimit-reset: { $ref: '#/components/headers/XRateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateResponse'
              example:
                id: 0f2d6c1a-8f4b-4a4e-9a8e-3b2c1d0e9f8a
                download_url: https://example-bucket.s3.amazonaws.com/pdfs/0f2d6c1a.pdf?X-Amz-Signature=...
                expires_in: 3600
                bytes: 18342
                filename: invoice-1024.pdf
                usage: { used: 4, limit: 100, period: "2026-07", tier: free }
        '400':
          $ref: '#/components/responses/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '413':
          $ref: '#/components/responses/Error'
        '429':
          description: Monthly quota exhausted for this API key's tier.
          headers:
            x-request-id: { $ref: '#/components/headers/XRequestId' }
            x-ratelimit-limit: { $ref: '#/components/headers/XRateLimitLimit' }
            x-ratelimit-remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
            x-ratelimit-reset: { $ref: '#/components/headers/XRateLimitReset' }
            retry-after: { $ref: '#/components/headers/RetryAfter' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: quota_exceeded
                  message: Monthly quota of 100 reached for tier "free".
                  hint: Upgrade your plan on Gumroad or wait until the next billing period.
                  request_id: 7b9d5c0e-1f2a-4c3b-9d8e-6f5a4b3c2d1e
                request_id: 7b9d5c0e-1f2a-4c3b-9d8e-6f5a4b3c2d1e
        '500':
          $ref: '#/components/responses/Error'
        '502':
          $ref: '#/components/responses/Error'
  /v1/usage:
    get:
      operationId: getUsage
      summary: Get current tier and monthly usage for the calling key.
      parameters:
        - $ref: '#/components/parameters/XRequestId'
      responses:
        '200':
          description: Usage info.
          headers:
            x-request-id: { $ref: '#/components/headers/XRequestId' }
            x-ratelimit-limit: { $ref: '#/components/headers/XRateLimitLimit' }
            x-ratelimit-remaining: { $ref: '#/components/headers/XRateLimitRemaining' }
            x-ratelimit-reset: { $ref: '#/components/headers/XRateLimitReset' }
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier: { type: string }
                  period: { type: string }
                  used: { type: integer }
                  limit: { type: integer }
                  remaining: { type: integer }
              example:
                tier: free
                period: "2026-07"
                used: 4
                limit: 100
                remaining: 96
        '401':
          $ref: '#/components/responses/Error'
  /v1/manifest:
    get:
      operationId: getManifest
      summary: Machine-readable capability manifest for agents.
      security: []
      responses:
        '200':
          description: Manifest.
          headers:
            x-request-id: { $ref: '#/components/headers/XRequestId' }
          content:
            application/json:
              schema:
                type: object
  /healthz:
    get:
      operationId: health
      summary: Health check.
      security: []
      responses:
        '200':
          description: OK
          headers:
            x-request-id: { $ref: '#/components/headers/XRequestId' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: api-key
  parameters:
    XRequestId:
      name: X-Request-Id
      in: header
      required: false
      description: >
        Optional client correlation id (letters, digits, dot, underscore,
        hyphen; max 128 chars). Echoed back in the x-request-id response
        header and as request_id in error bodies.
      schema: { type: string, pattern: '^[A-Za-z0-9._-]{1,128}$' }
  headers:
    XRequestId:
      description: >
        Correlation id for this request. Echoes the caller's X-Request-Id when
        provided, else the gateway request id. Matches request_id in error bodies.
      schema: { type: string }
      example: 7b9d5c0e-1f2a-4c3b-9d8e-6f5a4b3c2d1e
    XRateLimitLimit:
      description: Monthly request quota for the key's tier.
      schema: { type: string }
      example: "100"
    XRateLimitRemaining:
      description: Requests remaining in the current monthly period.
      schema: { type: string }
      example: "96"
    XRateLimitReset:
      description: Epoch seconds when the monthly quota resets (first of next UTC month).
      schema: { type: string }
      example: "1754006400"
    RetryAfter:
      description: Seconds to wait before retrying.
      schema: { type: string }
      example: "3600"
  responses:
    Error:
      description: >
        Error envelope. code is machine-readable; hint says how to fix the
        request; request_id matches the x-request-id response header.
      headers:
        x-request-id: { $ref: '#/components/headers/XRequestId' }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_request
              message: Provide exactly one of html, url, or markdown.
              hint: 'Send Content-Type: application/json with a JSON object.'
              request_id: 7b9d5c0e-1f2a-4c3b-9d8e-6f5a4b3c2d1e
            request_id: 7b9d5c0e-1f2a-4c3b-9d8e-6f5a4b3c2d1e
  schemas:
    GenerateRequest:
      type: object
      description: Provide exactly one of html, url, or markdown.
      properties:
        html:
          type: string
          description: Raw HTML document to render.
        url:
          type: string
          format: uri
          description: http(s) URL to render.
        markdown:
          type: string
          description: GitHub-flavored Markdown to render.
        filename:
          type: string
          description: Optional output filename.
        options:
          $ref: '#/components/schemas/Options'
    Options:
      type: object
      properties:
        format:
          type: string
          enum: [A4, A3, A5, Letter, Legal, Tabloid]
          default: A4
        landscape:
          type: boolean
        printBackground:
          type: boolean
          default: true
        scale:
          type: number
          minimum: 0.1
          maximum: 2
        margin:
          type: object
          properties:
            top: { type: string }
            bottom: { type: string }
            left: { type: string }
            right: { type: string }
        title:
          type: string
          description: Title for markdown documents.
        css:
          type: string
          description: Extra CSS for markdown documents.
        returnBase64:
          type: boolean
          description: Also include pdf_base64 in the response.
    GenerateResponse:
      type: object
      properties:
        id: { type: string }
        download_url: { type: string, format: uri }
        expires_in: { type: integer }
        bytes: { type: integer }
        filename: { type: string }
        pdf_base64: { type: string }
        usage:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
            period: { type: string }
            tier: { type: string }
    Error:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              type: string
              enum: [invalid_request, unauthorized, forbidden, quota_exceeded, payload_too_large, render_failed, internal_error]
            message: { type: string }
            hint: { type: string, nullable: true }
            request_id: { type: string }
        request_id: { type: string }
