openapi: 3.1.0
info:
  title: Pdfmark API
  version: 1.0.0
  summary: Pay-per-use PDF editing and document tracking.
  description: |
    Pdfmark is a small kit of single-purpose PDF tools at pdfmark.net.
    Endpoints below cover the agent-usable surface for both tools:
    edit & sign a PDF (download finished file), and send & track a PDF
    (share link with engagement report).

    Each tool action costs $2.99 USD and is gated by a Stripe Checkout
    payment. The checkout endpoints return a URL that requires a human
    cardholder; Pdfmark does not currently support programmatic payment.
  contact:
    name: Pdfmark
    url: https://pdfmark.net
servers:
  - url: https://pdfmark.net
    description: Production
tags:
  - name: edit
    description: Edit & sign PDF tool.
  - name: track
    description: Send & track a PDF tool.

paths:
  /api/upload:
    post:
      tags: [edit]
      summary: Upload a PDF and start an editor session.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The PDF file (≤ 25 MB).
              required: [file]
      responses:
        "200":
          description: Session created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                required: [id]
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMedia" }

  /api/sessions/{id}:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    get:
      tags: [edit]
      summary: Get the current editor session state.
      responses:
        "200":
          description: Session state.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SessionState" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [edit]
      summary: Update annotations or page rotations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                annotations:
                  type: array
                  items: { $ref: "#/components/schemas/Annotation" }
                pageRotations:
                  type: object
                  additionalProperties:
                    type: integer
                    enum: [0, 90, 180, 270]
      responses:
        "200":
          description: Updated session.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SessionState" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /api/sessions/{id}/merge:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    post:
      tags: [edit]
      summary: Append another PDF to the current session.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
              required: [file]
      responses:
        "200":
          description: Merged.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  pageCount: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/TooLarge" }

  /api/sessions/{id}/pages/reorder:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    post:
      tags: [edit]
      summary: Reorder or remove pages.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                order:
                  type: array
                  description: New page order as a list of original page indices (0-based). Indices may be omitted to delete pages.
                  items: { type: integer, minimum: 0 }
              required: [order]
      responses:
        "200":
          description: Reordered.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /api/checkout:
    post:
      tags: [edit]
      summary: Create a Stripe Checkout session for the editor download.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                sessionId: { type: string }
              required: [sessionId]
      responses:
        "200":
          description: Checkout URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    description: Open in a browser for a human cardholder to complete payment.
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }

  /api/download/{id}:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    get:
      tags: [edit]
      summary: Stream the final, paid-for PDF.
      responses:
        "200":
          description: Final PDF.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        "402":
          description: Payment required (session not yet paid).
        "404": { $ref: "#/components/responses/NotFound" }

  /api/track/upload:
    post:
      tags: [track]
      summary: Upload a PDF for the document tracker.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: The PDF file (≤ 50 MB).
              required: [file]
      responses:
        "200":
          description: Pending upload created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                required: [id]
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMedia" }

  /api/track/checkout:
    post:
      tags: [track]
      summary: Configure share settings and create a Stripe Checkout session.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                uploadId: { type: string }
                senderDisplayName:
                  type: string
                  minLength: 1
                  maxLength: 80
                customSlug:
                  type: string
                  pattern: "^[a-z0-9-]{3,40}$"
                  description: Optional custom share slug. Reserved words rejected.
                codeRequired:
                  type: boolean
                  description: If true, the recipient must enter an access code (emailed to the sender).
              required: [uploadId, senderDisplayName, codeRequired]
      responses:
        "200":
          description: Checkout URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: Slug already taken.

components:
  parameters:
    SessionId:
      name: id
      in: path
      required: true
      schema: { type: string }

  responses:
    BadRequest:
      description: Malformed request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    NotFound:
      description: Not found or expired.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    Conflict:
      description: Already paid / state conflict.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    TooLarge:
      description: File exceeds size limit.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }
    UnsupportedMedia:
      description: Non-PDF file.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ApiError" }

  schemas:
    ApiError:
      type: object
      properties:
        error: { type: string }
      required: [error]

    SessionState:
      type: object
      properties:
        id: { type: string }
        createdAt: { type: integer, description: Unix ms timestamp. }
        originalFilename: { type: string }
        paid: { type: boolean }
        paidAt: { type: integer, nullable: true }
        stripeCheckoutId: { type: string, nullable: true }
        annotations:
          type: array
          items: { $ref: "#/components/schemas/Annotation" }
        pageRotations:
          type: object
          additionalProperties:
            type: integer
            enum: [0, 90, 180, 270]
      required:
        - id
        - createdAt
        - originalFilename
        - paid
        - annotations
        - pageRotations

    AnnotationBase:
      type: object
      properties:
        id: { type: string }
        page: { type: integer, minimum: 0 }
        x: { type: number, minimum: 0, maximum: 100, description: "Percent of page width, 0–100." }
        y: { type: number, minimum: 0, maximum: 100, description: "Percent of page height, 0–100." }
        width: { type: number, minimum: 0.5, maximum: 100 }
        height: { type: number, minimum: 0.5, maximum: 100 }
      required: [id, page, x, y, width, height]

    Annotation:
      oneOf:
        - $ref: "#/components/schemas/TextAnnotation"
        - $ref: "#/components/schemas/DateAnnotation"
        - $ref: "#/components/schemas/SignatureAnnotation"
        - $ref: "#/components/schemas/ImageAnnotation"
        - $ref: "#/components/schemas/CheckAnnotation"
        - $ref: "#/components/schemas/RedactAnnotation"
      discriminator:
        propertyName: kind
        mapping:
          text: "#/components/schemas/TextAnnotation"
          date: "#/components/schemas/DateAnnotation"
          signature: "#/components/schemas/SignatureAnnotation"
          image: "#/components/schemas/ImageAnnotation"
          check: "#/components/schemas/CheckAnnotation"
          redact: "#/components/schemas/RedactAnnotation"

    TextAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: text }
            text: { type: string }
            fontSize: { type: number, minimum: 6, maximum: 120, description: "In PDF points. Server shrinks to fit box width." }
          required: [kind, text, fontSize]

    DateAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: date }
            text: { type: string }
            fontSize: { type: number, minimum: 6, maximum: 120 }
          required: [kind, text, fontSize]

    SignatureAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: signature }
            pngDataUrl:
              type: string
              description: "data:image/png;base64,... — the rasterized signature."
          required: [kind, pngDataUrl]

    ImageAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: image }
            pngDataUrl: { type: string }
          required: [kind, pngDataUrl]

    CheckAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: check }
          required: [kind]

    RedactAnnotation:
      allOf:
        - $ref: "#/components/schemas/AnnotationBase"
        - type: object
          properties:
            kind: { const: redact }
          required: [kind]
