openapi: 3.1.0
info:
  title: Anonymizer API
  version: "2026-10-30"
  description: The Anonymizer file metadata stripping REST API.
  contact:
    name: Zachary Bornheimer
    url: https://github.com/zachbornheimer/anonymizer
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
x-anonymizerReleasePhase: ga
x-anonymizerSpecAudience: public
x-version-history:
  - version: "2026-10-30"
    changes:
      - Bound retained results and replay records with shared 24-hour expiry.
      - Add storage_capacity_exceeded and method_not_allowed problem codes.
      - Preserve health and binary-download compatibility exceptions.
servers:
  - url: https://anonymize-api.zysys.org
    description: Production
  - url: http://127.0.0.1:{port}
    description: Local
    variables:
      port:
        default: "8080"
tags:
  - name: Health
    description: Liveness probes
  - name: Anonymizations
    description: File anonymization resources
paths:
  /healthz:
    get:
      operationId: getHealthz
      summary: Liveness probe
      description: |
        Returns 200 ok when API keys and required media binaries (qpdf, exiftool, ffmpeg)
        are ready, or 503 problem details when unconfigured or missing dependencies.
        No authentication is required.
      tags: [Health]
      security: []
      responses:
        "200":
          description: Process and dependencies are ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                status: ok
        "503":
          $ref: "#/components/responses/Error"
      x-codeSamples:
        - lang: curl
          source: |
            curl -sS https://anonymize-api.zysys.org/healthz
        - lang: go
          source: |
            resp, err := http.Get("https://anonymize-api.zysys.org/healthz")
            if err != nil {
                log.Fatal(err)
            }
            defer resp.Body.Close()
  /v1/anonymizations:
    post:
      operationId: createAnonymization
      summary: Create an anonymization
      description: |
        Strips file metadata and returns an anonymization in the five-field envelope.
        The same Idempotency-Key and body replay the accepted result for 24 hours
        (container idle sleep is configured to 24h to match; a cold container restart or
        redeployment clears in-container state). Capacity exhaustion returns 503 without
        evicting accepted results or consuming the rejected key.
      tags: [Anonymizations]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              additionalProperties: false
              properties:
                file:
                  type: string
                  format: binary
                  description: The file to strip of identifying metadata before sharing.
                type:
                  type: string
                  description: Optional MIME override when content sniffing would be wrong (for example audio/mpeg).
                  example: application/pdf
            encoding:
              file:
                contentType: application/octet-stream
      responses:
        "200":
          description: Anonymization completed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnonymizationEnvelope"
              example:
                object: anonymization
                request_id: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
                idempotency_key: 550e8400-e29b-41d4-a716-446655440000
                livemode: true
                data:
                  id: anon_4fjK2nQ7mX1a
                  status: completed
                  filename: anon_4fjK2nQ7mX1a.pdf
                  file_type: application/pdf
                  bytes: 48291
                  created: "2026-08-14T14:32:01Z"
                  sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
                  removed:
                    - info
                    - xmp
                    - annotations
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/UnsupportedType"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/StorageCapacityExceeded"
        default:
          $ref: "#/components/responses/Error"
      x-codeSamples:
        - lang: curl
          source: |
            curl -sS https://anonymize-api.zysys.org/v1/anonymizations \
              -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
              -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
              -F "file=@q3-board-notes.pdf;type=application/pdf"
        - lang: go
          source: |
            var body bytes.Buffer
            w := multipart.NewWriter(&body)
            part, _ := w.CreateFormFile("file", "q3-board-notes.pdf")
            io.Copy(part, file)
            w.Close()
            req, _ := http.NewRequest(http.MethodPost, base+"/v1/anonymizations", &body)
            req.Header.Set("Authorization", "Bearer "+os.Getenv("ANONYMIZER_LIVE_KEY"))
            req.Header.Set("Idempotency-Key", "550e8400-e29b-41d4-a716-446655440000")
            req.Header.Set("Content-Type", w.FormDataContentType())
            resp, err := http.DefaultClient.Do(req)
  /v1/anonymizations/{id}:
    post:
      operationId: retrieveAnonymization
      summary: Retrieve an anonymization
      description: |
        Returns the anonymization resource for the opaque id. This read-only POST
        lookup does not create a new result or require an Idempotency-Key.
      tags: [Anonymizations]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AnonymizationID"
      responses:
        "200":
          description: Resource found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnonymizationEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        default:
          $ref: "#/components/responses/Error"
      x-codeSamples:
        - lang: curl
          source: |
            curl -sS -X POST https://anonymize-api.zysys.org/v1/anonymizations/anon_4fjK2nQ7mX1a \
              -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY"
        - lang: go
          source: |
            req, _ := http.NewRequest(http.MethodPost, base+"/v1/anonymizations/"+id, nil)
            req.Header.Set("Authorization", "Bearer "+os.Getenv("ANONYMIZER_LIVE_KEY"))
            resp, err := http.DefaultClient.Do(req)
  /v1/anonymizations/{id}/content:
    post:
      operationId: downloadAnonymizationContent
      summary: Download anonymized bytes
      description: |
        Returns raw anonymized bytes with the stored MIME type and attachment headers.
        This established Stripe Files-style download is not a JSON envelope.
      tags: [Anonymizations]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/AnonymizationID"
      responses:
        "200":
          description: Anonymized file bytes
          content:
            application/octet-stream:
              schema:
                $ref: "#/components/schemas/FileContent"
            "*/*":
              schema:
                $ref: "#/components/schemas/FileContent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        default:
          $ref: "#/components/responses/Error"
      x-codeSamples:
        - lang: curl
          source: |
            curl -sS -X POST https://anonymize-api.zysys.org/v1/anonymizations/anon_4fjK2nQ7mX1a/content \
              -H "Authorization: Bearer $ANONYMIZER_LIVE_KEY" \
              -o q3-board-notes.anon.pdf
        - lang: go
          source: |
            req, _ := http.NewRequest(http.MethodPost, base+"/v1/anonymizations/"+id+"/content", nil)
            req.Header.Set("Authorization", "Bearer "+os.Getenv("ANONYMIZER_LIVE_KEY"))
            resp, err := http.DefaultClient.Do(req)
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key of the form anon_live_<secret> or anon_test_<secret>.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: UUID v4 that makes this create request safely retryable for 24 hours.
      schema:
        type: string
        format: uuid
        example: 550e8400-e29b-41d4-a716-446655440000
    AnonymizationID:
      name: id
      in: path
      required: true
      description: Opaque anonymization resource id returned by createAnonymization.
      schema:
        type: string
        example: anon_4fjK2nQ7mX1a
  schemas:
    FileContent:
      type: string
      format: binary
    HealthStatus:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          description: Always "ok" when the process is accepting connections.
          example: ok
    Anonymization:
      type: object
      additionalProperties: false
      required: [id, status, filename, file_type, bytes, created]
      properties:
        id:
          type: string
          description: Opaque resource id prefixed with anon_.
          example: anon_4fjK2nQ7mX1a
        status:
          type: string
          enum: [completed, failed]
          description: Terminal processing status for this anonymization.
          example: completed
        filename:
          type: string
          description: Neutral server-minted filename (<id>.<ext>) safe for downstream sharing without leaking the uploader's original basename.
          example: anon_4fjK2nQ7mX1a.pdf
        file_type:
          type: string
          description: Detected or overridden canonical MIME type of the anonymized payload.
          example: application/pdf
        bytes:
          type: integer
          format: int64
          description: Size in bytes of the anonymized content.
          example: 48291
        created:
          type: string
          format: date-time
          description: RFC 3339 timestamp when the resource was created.
          example: "2026-08-14T14:32:01Z"
        sha256:
          type: string
          description: Hex-encoded SHA-256 digest of the anonymized output bytes.
          example: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
        removed:
          type: array
          description: Metadata classes stripped and verified absent before completion.
          items:
            type: string
          example:
            - info
            - xmp
            - annotations
    BaseResponse:
      type: object
      additionalProperties: false
      required: [object, request_id, idempotency_key, livemode, data]
      properties:
        object:
          type: string
          description: Stable resource type discriminator.
          example: anonymization
        request_id:
          type: string
          description: Server-minted correlation id of the form req_<ulid>.
          example: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
        idempotency_key:
          type: string
          description: Echo of the request Idempotency-Key header, or empty when the operation did not require one.
          example: 550e8400-e29b-41d4-a716-446655440000
        livemode:
          type: boolean
          description: True when the bearer key starts with anon_live_.
          example: true
        data:
          type: object
    AnonymizationEnvelope:
      allOf:
        - $ref: "#/components/schemas/BaseResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Anonymization"
    ProblemDetails:
      type: object
      additionalProperties: false
      required: [title, status, code, request_id]
      properties:
        type:
          type: string
          description: Problem type URI; about:blank when no dedicated type exists.
          example: about:blank
        title:
          type: string
          description: Short human-readable summary of the problem type.
          example: Unauthorized
        status:
          type: integer
          description: HTTP status code for this occurrence.
          example: 401
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence.
          example: Provide a valid Authorization Bearer key.
        code:
          type: string
          description: Stable machine-readable error code.
          enum:
            - unauthorized
            - missing_idempotency_key
            - invalid_idempotency_key
            - missing_file
            - empty_file
            - invalid_multipart
            - idempotency_conflict
            - not_found
            - method_not_allowed
            - payload_too_large
            - unsupported_type
            - rate_limited
            - storage_capacity_exceeded
            - service_unconfigured
            - dependency_unavailable
            - internal_error
          example: unauthorized
        advice_code:
          type: string
          description: Machine-readable next action hint.
          example: check_credentials
        param:
          type: string
          description: JSON pointer or field name that failed validation, when applicable.
          example: Idempotency-Key
        request_id:
          type: string
          description: Correlation id for this failed request.
          example: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
  responses:
    Error:
      description: Error
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    Unauthorized:
      description: Missing or invalid bearer key
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Unauthorized
            status: 401
            code: unauthorized
            advice_code: check_credentials
            request_id: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
    BadRequest:
      description: Validation failed
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    IdempotencyConflict:
      description: Idempotency-Key reused with a different body
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Conflict
            status: 409
            code: idempotency_conflict
            advice_code: use_new_key
            request_id: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
    RateLimited:
      description: |
        This container is already anonymizing as many files as it can
        (ANONYMIZER_MAX_CONCURRENT). Retry after the `Retry-After` delay.
      headers:
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    StorageCapacityExceeded:
      description: |
        This instance cannot retain another result within its byte or result
        count budget. Accepted results are not evicted before their 24-hour
        expiry; retry later with the same key. A single oversized output may
        need a smaller input. The rejected key is not consumed.
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Service Unavailable
            status: 503
            code: storage_capacity_exceeded
            advice_code: retry_later
            request_id: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
    PayloadTooLarge:
      description: Body exceeds ANONYMIZER_MAX_BYTES
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
    UnsupportedType:
      description: Uploaded file format or codec is not supported for anonymization
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Unprocessable Entity
            status: 422
            code: unsupported_type
            advice_code: do_not_retry
            param: file
            request_id: req_01HZK2N5GQR9T8X4B6FJW3Y1AS
