openapi: 3.1.0
info:
  title: Bytario API
  version: 0.1.0
  summary: Privacy-first file utility API — same WASM engine as bytario.com
  description: |
    Paid B2B file utility API. Every endpoint is a stateless conversion
    over byte arrays — the same `@bytario/core` pure functions that power
    the consumer site at `bytario.com`, hosted in a Cloudflare Worker.

    ## Authentication
    All `/v1/*` endpoints require an API key supplied as a bearer token:
    `Authorization: Bearer sk_live_...`. Keys are issued after successful
    Stripe checkout and emailed to the billing contact.

    ## Rate limits
    Every plan has a sustained-throughput budget (`RateLimit-Limit` header)
    and a burst allowance enforced via a per-key token bucket. Responses
    carry `RateLimit-Limit` / `RateLimit-Remaining`; a rejected request
    gets HTTP 429 plus `Retry-After`.

    ## Quota
    Monthly request caps reset on the 1st of each month at 00:00 UTC.
    `X-Quota-Remaining` on every success response reflects the post-call
    remaining budget. Quota exhaustion returns HTTP 429 `quota_exceeded`.
  contact:
    name: Bytario Support
    email: support@bytario.com
  license:
    name: Proprietary — Copyright (c) 2026 DMG L&D, LLC. All rights reserved.
    url: https://bytario.com/terms

servers:
  - url: https://api.bytario.com
    description: Production

security:
  - bearerAuth: []

tags:
  - name: image
    description: Image conversion, resize, compress, metadata, favicon
  - name: pdf
    description: PDF merge / split / rotate / watermark / info / text / render / forms
  - name: qr
    description: QR code generation and decoding
  - name: barcode
    description: 1D and 2D barcode generation and decoding (non-QR)
  - name: render
    description: Vector → raster rendering

paths:
  /health:
    get:
      summary: Liveness probe
      security: []
      tags: [image]
      responses:
        '200':
          description: OK
          content:
            text/plain:
              schema: { type: string, example: ok }

  /v1/image/convert:
    post:
      tags: [image]
      summary: Convert an image between formats
      parameters:
        - { name: to, in: query, required: true, schema: { type: string, enum: [jpeg, png, webp, avif] } }
        - { name: quality, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200': { $ref: '#/components/responses/BinaryImage' }
        '400': { $ref: '#/components/responses/Error' }
        '401': { $ref: '#/components/responses/Error' }
        '429': { $ref: '#/components/responses/Error' }

  /v1/image/resize:
    post:
      tags: [image]
      summary: Resize with fit modes (cover/contain/fill/inside/outside)
      parameters:
        - { name: width, in: query, schema: { type: integer, minimum: 1 } }
        - { name: height, in: query, schema: { type: integer, minimum: 1 } }
        - { name: fit, in: query, schema: { type: string, enum: [cover, contain, fill, inside, outside] } }
        - { name: format, in: query, schema: { type: string, enum: [jpeg, png, webp, avif] } }
        - { name: quality, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200': { $ref: '#/components/responses/BinaryImage' }
        '400': { $ref: '#/components/responses/Error' }

  /v1/image/compress:
    post:
      tags: [image]
      summary: Recompress at target quality (optionally re-encode format)
      parameters:
        - { name: quality, in: query, required: true, schema: { type: integer, minimum: 1, maximum: 100 } }
        - { name: format, in: query, schema: { type: string, enum: [jpeg, png, webp, avif] } }
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200': { $ref: '#/components/responses/BinaryImage' }

  /v1/image/strip-metadata:
    post:
      tags: [image]
      summary: Remove EXIF / IPTC / XMP / GPS without re-encoding pixels
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200': { $ref: '#/components/responses/BinaryImage' }

  /v1/image/read-metadata:
    post:
      tags: [image]
      summary: Extract EXIF / GPS / camera info as structured JSON
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200':
          description: ImageMetadata
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImageMetadata' }

  /v1/image/favicon-set:
    post:
      tags: [image]
      summary: Generate a full favicon bundle (ICO + PNGs + manifest.json)
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200':
          description: Favicon bundle JSON (base64 encoded ICO + PNGs + manifest)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FaviconSetResult' }

  /v1/pdf/merge:
    post:
      tags: [pdf]
      summary: Merge multiple PDFs into one
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: array
                  items: { type: string, format: binary }
          application/json:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  items: { type: string, format: byte, description: base64-encoded PDF }
      responses:
        '200': { $ref: '#/components/responses/BinaryPdf' }

  /v1/pdf/split:
    post:
      tags: [pdf]
      summary: Extract one or more page ranges into separate PDFs
      parameters:
        - { name: ranges, in: query, required: true, schema: { type: string, example: '1-3,7-9' } }
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200':
          description: Array of base64-encoded PDFs
          content:
            application/json:
              schema:
                type: object
                properties:
                  pdfs:
                    type: array
                    items: { type: string, format: byte }

  /v1/pdf/rotate:
    post:
      tags: [pdf]
      summary: Rotate specified pages by 90, 180, or 270 degrees
      parameters:
        - { name: pages, in: query, schema: { type: string, example: '1,3,5', default: all } }
        - { name: degrees, in: query, required: true, schema: { type: integer, enum: [90, 180, 270] } }
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200': { $ref: '#/components/responses/BinaryPdf' }

  /v1/pdf/watermark:
    post:
      tags: [pdf]
      summary: Stamp text watermark on one or more pages
      parameters:
        - { name: text, in: query, required: true, schema: { type: string } }
        - { name: opacity, in: query, schema: { type: number, minimum: 0, maximum: 1 } }
        - { name: rotation, in: query, schema: { type: number } }
        - { name: fontSize, in: query, schema: { type: number, minimum: 1 } }
        - { name: pages, in: query, schema: { type: string, default: all } }
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200': { $ref: '#/components/responses/BinaryPdf' }

  /v1/pdf/info:
    post:
      tags: [pdf]
      summary: Page count, page sizes, and document metadata
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200':
          description: PdfInfo
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PdfInfo' }

  /v1/pdf/extract-text:
    post:
      tags: [pdf]
      summary: Pure-JS vector text extraction (no OCR)
      parameters:
        - { name: pages, in: query, schema: { type: string, example: '1-3 or 1,3,5' } }
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200':
          description: PdfTextResult
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PdfTextResult' }

  /v1/pdf/render-page:
    post:
      tags: [pdf]
      summary: Rasterize one or more pages to PNG / JPEG / WebP
      parameters:
        - { name: pages, in: query, schema: { type: string, example: '1,3' } }
        - { name: format, in: query, schema: { type: string, enum: [png, jpeg, webp], default: png } }
        - { name: dpi, in: query, schema: { type: integer, minimum: 72, maximum: 300 } }
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200':
          description: Array of rendered pages
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PdfRenderPageResult' }

  /v1/pdf/fill-form:
    post:
      tags: [pdf]
      summary: Fill AcroForm fields (optionally flatten afterwards)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pdf, fields]
              properties:
                pdf: { type: string, format: byte }
                fields: { type: object, additionalProperties: true }
                flatten: { type: boolean }
          multipart/form-data:
            schema:
              type: object
              required: [file, fields]
              properties:
                file: { type: string, format: binary }
                fields: { type: string, description: JSON string of field map }
                flatten: { type: string }
      responses:
        '200': { $ref: '#/components/responses/BinaryPdf' }

  /v1/pdf/flatten:
    post:
      tags: [pdf]
      summary: Flatten form fields (bake values in, remove editability)
      requestBody: { $ref: '#/components/requestBodies/BinaryPdf' }
      responses:
        '200': { $ref: '#/components/responses/BinaryPdf' }

  /v1/qr/generate:
    post:
      tags: [qr]
      summary: Generate a QR code as PNG or SVG
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/QrGenerateOptions' }
      responses:
        '200':
          description: PNG or SVG bytes
          content:
            image/png: { schema: { type: string, format: binary } }
            image/svg+xml: { schema: { type: string } }

  /v1/qr/read:
    post:
      tags: [qr]
      summary: Decode a QR code from an image
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200':
          description: QrReadResult
          content:
            application/json:
              schema:
                type: object
                properties:
                  text: { type: string }
                  format: { type: string }

  /v1/barcode/generate:
    post:
      tags: [barcode]
      summary: Generate a 1D or 2D barcode (EAN, UPC, Code 128, DataMatrix, PDF417, Aztec)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BarcodeGenerateOptions' }
      responses:
        '200':
          description: SVG bytes
          content:
            image/svg+xml: { schema: { type: string } }

  /v1/barcode/read:
    post:
      tags: [barcode]
      summary: Decode 1D/2D barcodes from an image
      requestBody: { $ref: '#/components/requestBodies/BinaryImage' }
      responses:
        '200':
          description: BarcodeReadResult
          content:
            application/json:
              schema:
                type: object
                properties:
                  codes:
                    type: array
                    items:
                      type: object
                      properties:
                        text: { type: string }
                        format: { type: string }

  /v1/render/svg-to-png:
    post:
      tags: [render]
      summary: Render an SVG to a PNG via resvg-wasm
      parameters:
        - { name: scale, in: query, schema: { type: number, minimum: 0.1, maximum: 10 } }
        - { name: width, in: query, schema: { type: integer, minimum: 1 } }
        - { name: height, in: query, schema: { type: integer, minimum: 1 } }
      requestBody:
        required: true
        content:
          image/svg+xml:
            schema: { type: string }
      responses:
        '200':
          description: PNG bytes
          content:
            image/png: { schema: { type: string, format: binary } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_live_...

  requestBodies:
    BinaryImage:
      required: true
      content:
        image/jpeg: { schema: { type: string, format: binary } }
        image/png:  { schema: { type: string, format: binary } }
        image/webp: { schema: { type: string, format: binary } }
        image/avif: { schema: { type: string, format: binary } }
        image/heic: { schema: { type: string, format: binary } }
        application/octet-stream: { schema: { type: string, format: binary } }
    BinaryPdf:
      required: true
      content:
        application/pdf: { schema: { type: string, format: binary } }
        application/octet-stream: { schema: { type: string, format: binary } }

  responses:
    BinaryImage:
      description: Converted image bytes
      headers:
        X-Quota-Remaining:
          schema: { type: integer }
        RateLimit-Limit:
          schema: { type: integer }
        RateLimit-Remaining:
          schema: { type: integer }
      content:
        image/png: { schema: { type: string, format: binary } }
        image/jpeg: { schema: { type: string, format: binary } }
        image/webp: { schema: { type: string, format: binary } }
        image/avif: { schema: { type: string, format: binary } }
    BinaryPdf:
      description: PDF bytes
      headers:
        X-Quota-Remaining: { schema: { type: integer } }
        RateLimit-Limit: { schema: { type: integer } }
        RateLimit-Remaining: { schema: { type: integer } }
      content:
        application/pdf:
          schema: { type: string, format: binary }
    Error:
      description: Standardized error envelope
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ApiError' }

  schemas:
    ApiError:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          enum:
            - unauthorized
            - subscription_inactive
            - quota_exceeded
            - rate_limited
            - invalid_input
            - unsupported_format
            - too_large
            - conversion_failed
            - internal_error
        message: { type: string }
        upgrade_url: { type: string, format: uri }

    ImageMetadata:
      type: object
      properties:
        width:  { type: integer }
        height: { type: integer }
        takenAt: { type: string, format: date-time }
        camera:
          type: object
          properties:
            make:  { type: string }
            model: { type: string }
            lens:  { type: string }
        gps:
          type: object
          properties:
            latitude:  { type: number }
            longitude: { type: number }
            altitude:  { type: number }
        exif:
          type: object
          additionalProperties: true

    FaviconSetResult:
      type: object
      properties:
        ico: { type: string, format: byte }
        pngs:
          type: object
          additionalProperties: { type: string, format: byte }
        manifest: { type: string }

    PdfInfo:
      type: object
      required: [pageCount, pageSizes, metadata, encrypted]
      properties:
        pageCount: { type: integer }
        pageSizes:
          type: array
          items:
            type: object
            properties:
              width: { type: number }
              height: { type: number }
        metadata:
          type: object
          properties:
            title:   { type: string }
            author:  { type: string }
            subject: { type: string }
            keywords: { type: string }
            producer: { type: string }
            creator:  { type: string }
        encrypted: { type: boolean }

    PdfTextResult:
      type: object
      properties:
        text:  { type: string }
        pages:
          type: array
          items: { type: string }

    PdfRenderPageResult:
      type: object
      properties:
        pages:
          type: array
          items:
            type: object
            properties:
              page:  { type: integer }
              bytes: { type: string, format: byte }
              width: { type: integer }
              height: { type: integer }

    QrGenerateOptions:
      type: object
      required: [text]
      properties:
        text:   { type: string }
        format: { type: string, enum: [png, svg], default: png }
        size:   { type: integer, minimum: 1 }
        margin: { type: integer, minimum: 0 }
        errorCorrection: { type: string, enum: [L, M, Q, H], default: M }
        foreground: { type: string, example: '#000000' }
        background: { type: string, example: '#ffffff' }

    BarcodeGenerateOptions:
      type: object
      required: [text, format]
      properties:
        text: { type: string }
        format:
          type: string
          enum: [ean13, ean8, upca, upce, code128, code39, itf14, itf, datamatrix, pdf417, aztec]
        output: { type: string, enum: [svg], default: svg }
        scale:  { type: integer }
        height: { type: integer }
        includeText: { type: boolean }
        foreground: { type: string }
        background: { type: string }
