openapi: 3.1.0
info:
  title: PixDoc API
  description: |
    Generate PDFs, screenshots, and OG images from HTML or URLs.

    ## Authentication

    All endpoints require an API key passed as a Bearer token:
    ```
    Authorization: Bearer pd_live_a3b2c4d5...
    ```
    The OG image endpoint accepts the key as a `?key=` query parameter (for use in meta tags).

    Create API keys at [pixdoc.dev/dashboard/keys](https://pixdoc.dev/dashboard/keys).

    ## Rate Limits

    | Plan | Renders/Month |
    |------|--------------|
    | Free | 100 |
    | Starter | 5,000 |
    | Pro | 25,000 |
    | Business | 100,000 |

    Check `X-Renders-Remaining` response header for current usage.
  version: 1.0.0
  contact:
    name: PixDoc Support
    url: https://pixdoc.dev
  license:
    name: MIT

servers:
  - url: https://pixdoc.dev
    description: Production

security:
  - BearerAuth: []

tags:
  - name: PDF
    description: Generate PDFs from HTML, URLs, or templates
  - name: Screenshot
    description: Capture screenshots of HTML or URLs
  - name: OG Image
    description: Generate Open Graph images from templates
  - name: Renders
    description: Access render history and generate signed download URLs
  - name: Assets
    description: Upload and manage custom fonts and images

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key in format `pd_live_...`

  schemas:
    PageFormat:
      oneOf:
        - type: string
          enum: [A4, Letter, Legal]
        - type: object
          required: [width, height]
          properties:
            width:
              type: string
              description: "Custom width (e.g., '210mm', '8.5in')"
            height:
              type: string
              description: "Custom height (e.g., '297mm', '11in')"

    Margins:
      type: object
      properties:
        top:
          type: string
          default: "20mm"
        right:
          type: string
          default: "20mm"
        bottom:
          type: string
          default: "20mm"
        left:
          type: string
          default: "20mm"

    PdfOptions:
      type: object
      properties:
        format:
          $ref: "#/components/schemas/PageFormat"
          default: A4
        landscape:
          type: boolean
          default: false
        margin:
          $ref: "#/components/schemas/Margins"
        printBackground:
          type: boolean
          default: true
          description: Print CSS backgrounds
        displayHeaderFooter:
          type: boolean
          default: false
        headerTemplate:
          type: string
          nullable: true
          description: HTML template for page header
        footerTemplate:
          type: string
          nullable: true
          description: HTML template for page footer
        scale:
          type: number
          minimum: 0.1
          maximum: 2.0
          default: 1
        enableJavaScript:
          type: boolean
          default: false
          description: Execute JavaScript before rendering
        waitForSelector:
          type: string
          nullable: true
          description: CSS selector to wait for before rendering
        waitForTimeout:
          type: integer
          nullable: true
          description: Milliseconds to wait after page load
        timeout:
          type: integer
          minimum: 1000
          maximum: 60000
          default: 30000
          description: Max render time in milliseconds

    ScreenshotOptions:
      type: object
      properties:
        width:
          type: integer
          minimum: 1
          default: 1280
          description: Viewport width in pixels
        height:
          type: integer
          minimum: 1
          default: 800
          description: Viewport height in pixels
        format:
          type: string
          enum: [png, jpeg, webp]
          default: png
        quality:
          type: integer
          minimum: 1
          maximum: 100
          default: 80
          description: JPEG/WebP quality
        fullPage:
          type: boolean
          default: false
          description: Capture full scrollable page
        selector:
          type: string
          nullable: true
          description: CSS selector to screenshot a specific element
        deviceScaleFactor:
          type: integer
          enum: [1, 2, 3]
          default: 1
          description: Retina scale factor
        darkMode:
          type: boolean
          default: false
          description: Emulate prefers-color-scheme dark
        blockAds:
          type: boolean
          default: false
          description: Block common ad/tracking domains
        injectCSS:
          type: string
          nullable: true
          description: Custom CSS to inject before capture
        waitForSelector:
          type: string
          nullable: true
        waitForTimeout:
          type: integer
          nullable: true
        timeout:
          type: integer
          minimum: 1000
          maximum: 60000
          default: 30000
        cookies:
          type: array
          items:
            type: object
            required: [name, value, domain]
            properties:
              name:
                type: string
              value:
                type: string
              domain:
                type: string
          default: []
          description: Cookies to set before navigation
        extraHeaders:
          type: object
          additionalProperties:
            type: string
          default: {}
          description: Additional HTTP headers

    PdfRequest:
      type: object
      properties:
        html:
          type: string
          description: HTML content to render
        url:
          type: string
          format: uri
          description: URL to render
        template_id:
          type: string
          format: uuid
          description: Template ID to render
        template_version:
          type: integer
          minimum: 1
          description: "Pin a specific template version. If omitted, the latest version is used."
        data:
          type: object
          additionalProperties:
            type: string
          description: Template variable values
        options:
          $ref: "#/components/schemas/PdfOptions"
        metadata:
          type: object
          nullable: true
          description: "Arbitrary JSON metadata (max 1KB) attached to the render. Returned in webhook payloads and polling responses."
          additionalProperties: true
        webhook_url:
          type: string
          format: uri
          maxLength: 2048
          description: "HTTPS URL to receive the render result via webhook. Only used when async is true. Requires paid plan."
        webhook_secret:
          type: string
          minLength: 8
          maxLength: 256
          description: "Secret key for HMAC-SHA256 webhook payload signing. Recommended for verifying webhook authenticity."
        async:
          type: boolean
          description: "Set to true to enable async mode. Returns 202 with request_id and poll_url. Optionally combine with webhook_url for delivery. Requires paid plan."
      description: Exactly one of `html`, `url`, or `template_id` is required.

    ScreenshotRequest:
      type: object
      properties:
        html:
          type: string
          description: HTML content to render
        url:
          type: string
          format: uri
          description: URL to render
        options:
          $ref: "#/components/schemas/ScreenshotOptions"
        metadata:
          type: object
          nullable: true
          description: "Arbitrary JSON metadata (max 1KB) attached to the render. Returned in webhook payloads and polling responses."
          additionalProperties: true
        webhook_url:
          type: string
          format: uri
          maxLength: 2048
          description: "HTTPS URL to receive the render result via webhook. Only used when async is true. Requires paid plan."
        webhook_secret:
          type: string
          minLength: 8
          maxLength: 256
          description: "Secret key for HMAC-SHA256 webhook payload signing. Recommended for verifying webhook authenticity."
        async:
          type: boolean
          description: "Set to true to enable async mode. Returns 202 with request_id and poll_url. Optionally combine with webhook_url for delivery. Requires paid plan."
      description: Exactly one of `html` or `url` is required.

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, status, request_id]
          properties:
            code:
              type: string
              enum:
                - AUTH_MISSING
                - AUTH_INVALID
                - AUTH_REVOKED
                - PLAN_LIMIT_EXCEEDED
                - RATE_LIMITED
                - VALIDATION_ERROR
                - RENDER_TIMEOUT
                - RENDER_FAILED
                - SSRF_BLOCKED
                - TEMPLATE_NOT_FOUND
                - RENDERER_UNAVAILABLE
            message:
              type: string
              description: Human-readable error message
            status:
              type: integer
              description: HTTP status code
            request_id:
              type: string
              description: Unique request ID for support
              example: req_a1b2c3d4e5f67890
            details:
              type: object
              description: Additional error context

  headers:
    X-Request-Id:
      schema:
        type: string
      description: Unique request identifier
      example: req_a1b2c3d4e5f67890
    X-Render-Duration-Ms:
      schema:
        type: integer
      description: Rendering time in milliseconds
    X-Renders-Remaining:
      schema:
        type: integer
      description: Remaining renders in current billing month
    X-Overage-Renders:
      schema:
        type: integer
      description: Number of overage renders used this month (0 if within plan limits)

paths:
  /api/v1/pdf:
    post:
      summary: Generate PDF
      description: |
        Render a PDF from HTML content, a public URL, or a saved template.
        Returns the PDF as a binary response.
      operationId: renderPdf
      tags: [PDF]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
          description: |
            Unique key to ensure idempotent requests. If the same key is sent again
            within 24 hours, the cached result is returned without re-rendering.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PdfRequest"
            examples:
              html:
                summary: From HTML
                value:
                  html: "<h1>Invoice #1042</h1><p>Amount: $500</p>"
                  options:
                    format: A4
                    printBackground: true
              url:
                summary: From URL
                value:
                  url: "https://example.com/report"
                  options:
                    format: Letter
                    landscape: true
              template:
                summary: From template
                value:
                  template_id: "550e8400-e29b-41d4-a716-446655440000"
                  data:
                    company: "Acme Corp"
                    amount: "$5,000.00"
                  options:
                    format: A4
      responses:
        "200":
          description: PDF generated
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
            X-Render-Duration-Ms:
              $ref: "#/components/headers/X-Render-Duration-Ms"
            X-Renders-Remaining:
              $ref: "#/components/headers/X-Renders-Remaining"
            X-Overage-Renders:
              $ref: "#/components/headers/X-Overage-Renders"
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
              description: Whether the result was served from the idempotency cache
            X-Idempotency-Key:
              schema:
                type: string
              description: Echoes back the idempotency key from the request
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "202":
          description: Async render queued (when async is true)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      request_id:
                        type: string
                        example: "req_a1b2c3d4e5f67890"
                      status:
                        type: string
                        example: "queued"
                      poll_url:
                        type: string
                        example: "/api/v1/renders/req_a1b2c3d4e5f67890"
        "400":
          description: Validation error or SSRF blocked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "408":
          description: Render timed out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Render failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Renderer unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/screenshot:
    post:
      summary: Take screenshot
      description: |
        Capture a screenshot of HTML content or a public URL.
        Returns the image as a binary response (PNG, JPEG, or WebP).
      operationId: renderScreenshot
      tags: [Screenshot]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
            minLength: 1
            maxLength: 255
          description: |
            Unique key to ensure idempotent requests. If the same key is sent again
            within 24 hours, the cached result is returned without re-rendering.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScreenshotRequest"
            examples:
              html:
                summary: From HTML
                value:
                  html: "<div style='padding:40px'><h1>Hello</h1></div>"
                  options:
                    width: 1280
                    height: 800
                    format: png
              url:
                summary: Full page screenshot
                value:
                  url: "https://example.com"
                  options:
                    fullPage: true
                    format: jpeg
                    quality: 90
              retina_dark:
                summary: Retina dark mode
                value:
                  url: "https://example.com"
                  options:
                    deviceScaleFactor: 2
                    darkMode: true
      responses:
        "200":
          description: Screenshot captured
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
            X-Render-Duration-Ms:
              $ref: "#/components/headers/X-Render-Duration-Ms"
            X-Renders-Remaining:
              $ref: "#/components/headers/X-Renders-Remaining"
            X-Overage-Renders:
              $ref: "#/components/headers/X-Overage-Renders"
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
              description: Whether the result was served from the idempotency cache
            X-Idempotency-Key:
              schema:
                type: string
              description: Echoes back the idempotency key from the request
          content:
            image/png:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
        "202":
          description: Async render queued (when async is true)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      request_id:
                        type: string
                        example: "req_a1b2c3d4e5f67890"
                      status:
                        type: string
                        example: "queued"
                      poll_url:
                        type: string
                        example: "/api/v1/renders/req_a1b2c3d4e5f67890"
        "400":
          description: Validation error or SSRF blocked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "408":
          description: Render timed out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Render failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Renderer unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/og/{template}:
    get:
      summary: Generate OG image
      description: |
        Generate a cached Open Graph image from a saved template.
        Pass template variables as query parameters.

        Designed for use in HTML meta tags:
        ```html
        <meta property="og:image" content="https://pixdoc.dev/api/v1/og/blog-post?key=pd_live_...&title=My+Post" />
        ```

        **Caching:** Results are cached for 24 hours. Cache key is derived from template + variables.
        Use `&v=2` to bust the cache after template updates.

        **Billing:** Cache hits are free. Only cache misses count as renders.
      operationId: renderOgImage
      tags: [OG Image]
      security: []
      parameters:
        - name: template
          in: path
          required: true
          schema:
            type: string
          description: Template slug (e.g., "blog-post", "social-profile")
        - name: key
          in: query
          required: true
          schema:
            type: string
          description: API key
        - name: v
          in: query
          schema:
            type: string
          description: Cache version (change to invalidate cache)
      responses:
        "200":
          description: OG image generated or served from cache
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
            X-Cache:
              schema:
                type: string
                enum: [HIT, MISS]
              description: Whether the image was served from cache
            Cache-Control:
              schema:
                type: string
              example: "public, max-age=86400"
            X-Overage-Renders:
              $ref: "#/components/headers/X-Overage-Renders"
          content:
            image/png:
              schema:
                type: string
                format: binary
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Template not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/renders/{requestId}:
    get:
      summary: Get render status
      description: Check the status of a render and get the output URL when completed. Use as a polling fallback for async renders.
      tags: [Renders]
      security:
        - BearerAuth: []
      parameters:
        - name: requestId
          in: path
          required: true
          schema:
            type: string
          description: The request_id returned from the async render submission
      responses:
        "200":
          description: Render status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      request_id:
                        type: string
                      status:
                        type: string
                        enum: [queued, rendering, completed, failed]
                      type:
                        type: string
                        enum: [pdf, screenshot, og]
                      metadata:
                        type: object
                        nullable: true
                        description: "Arbitrary JSON metadata attached to the render request, if provided."
                        additionalProperties: true
                      output_url:
                        type: string
                        nullable: true
                        description: Signed URL (1-hour expiry) when status is completed
                      output_size_bytes:
                        type: integer
                        nullable: true
                      render_duration_ms:
                        type: integer
                        nullable: true
                      error_message:
                        type: string
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
        "404":
          description: Render not found

  /api/v1/renders:
    get:
      tags: [Renders]
      summary: List render history
      description: Returns a paginated list of your renders, most recent first.
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [pdf, screenshot, og]
          description: Filter by render type
        - name: status
          in: query
          schema:
            type: string
            enum: [completed, failed, queued, rendering]
          description: Filter by status
        - name: signed
          in: query
          schema:
            type: boolean
            default: false
          description: When true, output_url fields contain time-limited signed URLs (1-hour expiry) instead of storage paths
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
          description: Results per page
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
          description: Number of results to skip
      responses:
        "200":
          description: Render list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        type:
                          type: string
                          enum: [pdf, screenshot, og]
                        status:
                          type: string
                          enum: [completed, failed, queued, rendering]
                        input_type:
                          type: string
                          enum: [html, url, template]
                        output_url:
                          type: string
                          nullable: true
                          description: Storage path (or signed URL when ?signed=true)
                        output_size_bytes:
                          type: integer
                          nullable: true
                        render_duration_ms:
                          type: integer
                          nullable: true
                        request_id:
                          type: string
                        created_at:
                          type: string
                          format: date-time
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/renders/{id}/signed-url:
    post:
      tags: [Renders]
      summary: Generate a signed download URL
      description: |
        Generate a time-limited signed URL for a render output. The URL can be safely
        shared with end users to download PDFs, screenshots, or images without needing
        an API key. Default expiry is 1 hour, configurable up to 90 days.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Render ID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                expiresIn:
                  type: integer
                  default: 3600
                  minimum: 60
                  maximum: 7776000
                  description: URL expiry time in seconds (default 3600, max 7776000 = 90 days)
      responses:
        "200":
          description: Signed URL generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      signed_url:
                        type: string
                        format: uri
                        description: Time-limited download URL (direct Supabase Storage access)
                      share_url:
                        type: string
                        format: uri
                        description: Revocable proxy URL for sharing with third parties
                      expires_at:
                        type: string
                        format: date-time
                        description: ISO 8601 timestamp when the URL expires
              example:
                success: true
                data:
                  signed_url: "https://project.supabase.co/storage/v1/object/sign/render-outputs/..."
                  share_url: "https://pixdoc.dev/api/share/a1b2c3d4-..."
                  expires_at: "2026-03-28T13:00:00.000Z"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Render not found or no output available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /api/v1/assets:
    get:
      tags: [Assets]
      summary: List uploaded assets
      description: Returns all assets uploaded by the authenticated user.
      responses:
        "200":
          description: Asset list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      assets:
                        type: array
                        items:
                          type: object
                          properties:
                            id:
                              type: string
                              format: uuid
                            filename:
                              type: string
                            content_type:
                              type: string
                            size_bytes:
                              type: integer
                            asset_type:
                              type: string
                              enum: [font, image, other]
                            font_family:
                              type: string
                              nullable: true
                            url:
                              type: string
                              format: uri
                            created_at:
                              type: string
                              format: date-time
                      storage:
                        type: object
                        properties:
                          used:
                            type: integer
                          limit:
                            type: integer
    post:
      tags: [Assets]
      summary: Upload an asset
      description: Upload a font or image file. Max 10 MB per file. Storage limits apply per plan.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: Font (.woff2, .woff, .ttf, .otf) or image (.png, .jpg, .svg, .webp) file
      responses:
        "200":
          description: Asset uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      filename:
                        type: string
                      url:
                        type: string
                        format: uri
                      asset_type:
                        type: string
                      font_family:
                        type: string
                        nullable: true
        "400":
          description: Invalid file or storage quota exceeded
        "401":
          description: Unauthorized

  /api/v1/assets/{id}:
    delete:
      tags: [Assets]
      summary: Delete an asset
      description: Permanently delete an uploaded asset.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Asset deleted
        "404":
          description: Asset not found
