openapi: 3.0.3
info:
  title: FileDroppr API
  description: >
    Upload PDFs and get trackable share links programmatically. Authenticate
    with a personal API key (create one at https://filedroppr.com/account under
    API keys) sent as a bearer token.
  version: "1.0.0"
  contact:
    email: support@filedroppr.com
servers:
  - url: https://filedroppr.com
security:
  - bearerAuth: []
tags:
  - name: Files
  - name: Events
  - name: Account
paths:
  /api/v1/files:
    post:
      tags: [Files]
      summary: Upload a PDF
      description: Upload a PDF as multipart form-data. Returns the new file's UUID; the share link is `https://filedroppr.com/{uuid}`.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The PDF file. Must be application/pdf.
      responses:
        "200":
          description: Uploaded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadResult"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "429": { $ref: "#/components/responses/RateLimited" }
    get:
      tags: [Files]
      summary: List your files
      responses:
        "200":
          description: The caller's files, newest first
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/File" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/files/{uuid}:
    parameters:
      - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Files]
      summary: Get a file's details
      responses:
        "200":
          description: File details
          content:
            application/json:
              schema: { $ref: "#/components/schemas/File" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Files]
      summary: Delete a file
      responses:
        "200": { description: Deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/files/{uuid}/settings:
    parameters:
      - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
    put:
      tags: [Files]
      summary: Update a file's settings
      description: >
        Set the viewer mode, password, download permission, and expiry. Omitted
        fields keep their current value. Some settings are plan-gated and return
        403 below the required plan.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FileSettings" }
      responses:
        "200": { description: Settings applied }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/files/{uuid}/qr-code:
    parameters:
      - { name: uuid, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [Files]
      summary: QR code for the share link
      responses:
        "200":
          description: PNG QR code encoding the share URL
          content:
            image/png:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
  /api/v1/events:
    get:
      tags: [Events]
      summary: Recent read events
      description: >
        Reads (view sessions) across your files, newest first. Each event has a
        stable `id`; poll with `since` to fetch only newer events.
      parameters:
        - { name: since, in: query, required: false, schema: { type: integer, default: 0 }, description: Return events with id greater than this. }
        - { name: limit, in: query, required: false, schema: { type: integer, default: 50, maximum: 100 }, description: Max events to return (1-100). }
      responses:
        "200":
          description: Read events
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/ReadEvent" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/user:
    get:
      tags: [Account]
      summary: Your account and plan
      responses:
        "200":
          description: Account
          content:
            application/json:
              schema: { $ref: "#/components/schemas/User" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /oauth/authorize:
    get:
      tags: [OAuth]
      summary: Authorization endpoint (Authorization Code + PKCE)
      description: Redirect the user's browser here to obtain consent. On approval, redirects back to redirect_uri with ?code= (and ?state=).
      security: []
      parameters:
        - { name: response_type, in: query, required: true, schema: { type: string, enum: [code] } }
        - { name: client_id, in: query, required: true, schema: { type: string } }
        - { name: redirect_uri, in: query, required: true, schema: { type: string } }
        - { name: scope, in: query, required: true, schema: { type: string }, description: Space-delimited. }
        - { name: state, in: query, required: false, schema: { type: string } }
        - { name: code_challenge, in: query, required: true, schema: { type: string } }
        - { name: code_challenge_method, in: query, required: true, schema: { type: string, enum: [S256] } }
      responses:
        "302": { description: Redirect to the consent screen, login, or back to redirect_uri with code/error. }
  /oauth/token:
    post:
      tags: [OAuth]
      summary: Token endpoint
      description: Exchanges an authorization code (with PKCE code_verifier) or a refresh token for tokens. Client auth via HTTP Basic or body params.
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                grant_type: { type: string, enum: [authorization_code, refresh_token] }
                code: { type: string }
                redirect_uri: { type: string }
                code_verifier: { type: string }
                refresh_token: { type: string }
                scope: { type: string, description: Optional down-scope on refresh. }
                client_id: { type: string }
                client_secret: { type: string, description: Confidential clients only. }
      responses:
        "200":
          description: Token response
          content:
            application/json:
              schema:
                type: object
                properties:
                  access_token: { type: string }
                  token_type: { type: string, example: Bearer }
                  expires_in: { type: integer }
                  refresh_token: { type: string }
                  scope: { type: string }
        "400": { description: invalid_request / invalid_grant / unsupported_grant_type }
        "401": { description: invalid_client }
  /oauth/revoke:
    post:
      tags: [OAuth]
      summary: Revoke a token (RFC 7009)
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                token: { type: string }
                token_type_hint: { type: string, enum: [access_token, refresh_token] }
                client_id: { type: string }
                client_secret: { type: string }
      responses:
        "200": { description: Always returns 200, even for unknown tokens. }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "An API key (fk_live_...) or OAuth access token (fd_at_...) sent as: Authorization: Bearer <token>"
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://filedroppr.com/oauth/authorize
          tokenUrl: https://filedroppr.com/oauth/token
          refreshUrl: https://filedroppr.com/oauth/token
          scopes:
            files:read: See your files and share links
            files:write: Upload, configure, and delete files
            readership:read: See who read your documents
            profile: See your account email and plan
  responses:
    BadRequest:
      description: Invalid request (e.g. not a PDF, malformed body).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unauthorized:
      description: Missing, malformed, invalid, or revoked API key.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Forbidden:
      description: Not allowed (ownership, or a setting above your plan).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: No such file (or not owned by you).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    TooLarge:
      description: The file is larger than your plan allows.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    RateLimited:
      description: Rate limit exceeded (120 requests per minute per key).
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    UploadResult:
      type: object
      properties:
        uuid: { type: string, format: uuid }
        message: { type: string, example: PDF uploaded successfully }
    File:
      type: object
      properties:
        uuid: { type: string, format: uuid }
        originalName: { type: string, example: proposal.pdf }
        size: { type: integer, format: int64, description: Bytes }
        mimeType: { type: string, example: application/pdf }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        deleteAt: { type: string, format: date-time, nullable: true, description: Absent when the file never expires. }
        viewCount: { type: integer }
        totalPages: { type: integer }
        viewerMode: { type: string, enum: [standard, flipbook, swipe, magazine] }
        protected: { type: boolean, description: True if a password is set. }
        allowDownload: { type: boolean }
        requireEmail: { type: boolean }
        notifyOnView: { type: boolean }
    FileSettings:
      type: object
      description: All fields optional; omitted fields keep their current value.
      properties:
        viewerMode: { type: string, enum: [standard, flipbook, swipe, magazine] }
        password: { type: string, description: "Non-empty to protect (Basic+); empty string to clear." }
        allowDownload: { type: boolean }
        requireEmail: { type: boolean, description: Gate viewing behind an email (Pro). }
        notifyOnView: { type: boolean, description: Email you on each read (Basic+). }
        deleteAt: { type: string, format: date-time, description: "Fixed expiry. Capped by plan: free 7 days, basic 365 days." }
        neverExpire: { type: boolean, description: Permanent link (Pro). Overrides deleteAt. }
    ReadEvent:
      type: object
      properties:
        id: { type: integer, description: Stable event id for polling/dedup. }
        fileUuid: { type: string, format: uuid }
        fileName: { type: string }
        readAt: { type: string, format: date-time }
        country: { type: string, example: United Kingdom }
        pagesViewed: { type: integer }
        timeMs: { type: integer, description: Time spent reading, milliseconds. }
    User:
      type: object
      properties:
        name: { type: string }
        email: { type: string, format: email }
        subscription: { type: string, enum: [free, basic, pro] }
        isAdmin: { type: boolean }
        created_at: { type: string, format: date-time }
    Error:
      type: object
      description: Errors are returned as a plain-text message with the HTTP status.
      properties:
        message: { type: string }
