openapi: 3.0.3
info:
  title: hushvert Conversion API
  version: "1.0.0"
  description: >
    Convert the file formats a browser cannot do, on the server: office documents
    to PDF, PDF to Word, document interchange (Markdown, HTML, EPUB, LaTeX, RST),
    and video transcodes. Everything a browser CAN do (images, HEIC, audio,
    archives, PDF page operations) is free and client-side in the open-source
    @hushvert/engine SDK and is intentionally not part of this API.

    Flow: (1) POST /v1/conversions with the pair and the byte count; you get back a
    jobId and a presigned uploadUrl. (2) PUT the file bytes to uploadUrl. (3) Poll
    GET /v1/conversions/{id} until status is "done", then download the presigned
    downloadUrl. Inputs are deleted the moment the conversion finishes; outputs are
    kept about an hour, then deleted.
  contact:
    name: hushvert developers
    url: https://hushvert.com/for-developers
servers:
  - url: https://hushvert.com/api
security:
  - bearerAuth: []
paths:
  /v1/formats:
    get:
      operationId: listFormats
      summary: List supported server conversions
      description: Public. Returns the server-only pairs this API converts, from the formats matrix.
      security: []
      responses:
        "200":
          description: The supported conversion pairs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  pairs:
                    type: array
                    items:
                      type: object
                      properties:
                        from: { type: string, example: docx }
                        to: { type: string, example: pdf }
                        pair: { type: string, example: docx-to-pdf }
                        label: { type: string, example: DOCX to PDF }
                        freeMaxBytes: { type: integer, example: 52428800 }
                        freeMaxMB: { type: integer, example: 50 }
                        costCredits: { type: integer, example: 1 }
  /v1/conversions:
    post:
      operationId: submitConversion
      summary: Submit a conversion
      description: >
        Declare a conversion. Returns a jobId and a presigned uploadUrl. PUT the
        file bytes to uploadUrl next, then poll the job. Send an Idempotency-Key to
        make a retry safe (same job, charged once).
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema: { type: string }
          description: Any unique string (such as a UUID). A retry with the same key returns the same job and never charges twice.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pair, bytes]
              properties:
                pair:
                  type: string
                  description: The conversion pair slug (see GET /v1/formats), e.g. "docx-to-pdf".
                  example: docx-to-pdf
                bytes:
                  type: integer
                  description: The exact byte length of the file you will upload.
                  example: 18234
      responses:
        "201":
          description: Job created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmitResult" }
        "200":
          description: Idempotent replay of an earlier identical submit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SubmitResult" }
        "400":
          description: bad-request, unknown-pair, or client-pair (a browser pair; use the free SDK).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "401":
          description: Missing or invalid API key (unauthenticated).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "402":
          description: Free monthly allowance used; add credits (require-credits).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "413":
          description: File exceeds the size cap for the pair (too-large).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "429":
          description: Rate limited. Honor the Retry-After header.
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /v1/conversions/{id}:
    get:
      operationId: getConversion
      summary: Poll a conversion and get the result
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Job status. When status is "done", downloadUrl is a presigned link valid about an hour.
          content:
            application/json:
              schema:
                type: object
                properties:
                  jobId: { type: string }
                  status: { type: string, enum: [queued, processing, done, failed], example: done }
                  downloadUrl: { type: string, nullable: true }
                  error: { type: string, nullable: true }
                  expiresAt: { type: string, nullable: true }
        "403":
          description: The job belongs to another account (forbidden).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
        "404":
          description: No job with that id (not-found).
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /v1/usage:
    get:
      operationId: getUsage
      summary: Usage and balance
      description: Read the account free allowance remaining, credit balance, and current billing window.
      responses:
        "200":
          description: Usage snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  freeTier:
                    type: object
                    properties:
                      jobsPerMonth: { type: integer }
                      jobsRemaining: { type: integer }
                  creditsBalance: { type: number }
                  currentWindow:
                    type: object
                    properties:
                      windowStart: { type: string }
                      account:
                        type: object
                        properties:
                          jobs: { type: integer }
                          bytes: { type: integer }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Your hv_live_ API key as a bearer token. Keep it server-side; never ship it to a browser.
  schemas:
    SubmitResult:
      type: object
      properties:
        jobId: { type: string }
        uploadUrl: { type: string, description: Presigned URL. PUT the file bytes here next. }
        pollUrl: { type: string, example: /api/v1/conversions/abc123 }
        idempotent: { type: boolean, nullable: true }
    Error:
      type: object
      properties:
        error: { type: string, description: Human-readable message. }
        code: { type: string, description: Stable machine code to branch on (e.g. unknown-pair, require-credits, too-large). }
