openapi: 3.1.0
info:
  title: freebin.org API
  version: 1.0.0
  description: |
    Manage bins and inspect captured development requests. Management endpoints
    are versioned under /api/v1. Capture endpoints use /b/{binId}/{path} and
    require a user API key belonging to the destination bin owner for every
    capture method. All bin-management routes use /api/v1.
  license:
    name: MIT
    identifier: MIT
servers:
  - url: https://freebin.org
security:
  - bearerAuth: []
tags:
  - name: Configuration
  - name: Authentication
  - name: Account
  - name: Bins
  - name: Requests
  - name: Sharing
  - name: Capture
paths:
  /api/config:
    get:
      tags: [Configuration]
      summary: Get public browser configuration
      description: Returns signup availability and public telemetry configuration. Secret server keys are never exposed.
      operationId: getPublicConfig
      security: []
      responses:
        "200":
          description: Public configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicConfig"
  /api/auth/register:
    post:
      tags: [Authentication]
      summary: Register an account
      description: Registration must be enabled. A successful response starts a 30-day browser session.
      operationId: register
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterInput"
      responses:
        "201":
          $ref: "#/components/responses/UserSession"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/Unavailable"
  /api/auth/login:
    post:
      tags: [Authentication]
      summary: Sign in
      description: Validates an email and password and starts a 30-day browser session.
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LoginInput"
      responses:
        "200":
          $ref: "#/components/responses/UserSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/auth/logout:
    post:
      tags: [Authentication]
      summary: Sign out
      description: Deletes the current server session, when present, and clears its browser cookie.
      operationId: logout
      security:
        - cookieAuth: []
        - {}
      responses:
        "200":
          $ref: "#/components/responses/Ok"
  /api/me:
    get:
      tags: [Account]
      summary: Get the current account
      description: Returns account storage usage and owned bins. Signed-out callers receive a successful response with user set to null.
      operationId: getCurrentAccount
      security:
        - cookieAuth: []
        - {}
      responses:
        "200":
          description: Current account state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountResponse"
    delete:
      tags: [Account]
      summary: Permanently delete the current account
      description: Deletes the signed-in account and clears its browser session.
      operationId: deleteCurrentAccount
      security:
        - cookieAuth: []
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/account/token:
    get:
      tags: [Account]
      summary: List API keys
      description: Returns key metadata only; complete token values are never returned after creation.
      operationId: listApiKeys
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Active API-key metadata
          content:
            application/json:
              schema:
                type: object
                required: [keys]
                properties:
                  keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Account]
      summary: Create an API key
      description: Accounts may have five active API keys. The complete token is returned only in this response.
      operationId: createApiKey
      security:
        - cookieAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  maxLength: 60
                  default: API key
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                required: [key, token]
                properties:
                  key:
                    $ref: "#/components/schemas/ApiKey"
                  token:
                    type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
    delete:
      tags: [Account]
      summary: Revoke an API key
      operationId: revokeApiKey
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id:
                  type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/bins:
    get:
      tags: [Bins]
      summary: List bins
      operationId: listBins
      responses:
        "200":
          description: Bins belonging to the authenticated user
          content:
            application/json:
              schema:
                type: object
                required: [bins]
                properties:
                  bins:
                    type: array
                    items:
                      $ref: "#/components/schemas/BinSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Bins]
      summary: Create a bin
      operationId: createBin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, termsAccepted]
              properties:
                name:
                  type: string
                  maxLength: 60
                termsAccepted:
                  type: boolean
                  const: true
      responses:
        "201":
          description: Bin created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateBinResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Five-bin account limit reached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/RateLimited"
  /api/v1/bins/{binId}:
    parameters:
      - $ref: "#/components/parameters/BinId"
    get:
      tags: [Bins]
      summary: Get bin configuration
      operationId: getBin
      responses:
        "200":
          description: Bin configuration
          content:
            application/json:
              schema:
                type: object
                properties:
                  bin:
                    $ref: "#/components/schemas/Bin"
        "403":
          $ref: "#/components/responses/Forbidden"
    patch:
      tags: [Bins]
      summary: Customize the capture response
      description: Response body, content type, and serialized response headers are each limited to 500 UTF-8 bytes.
      operationId: updateBinResponse
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResponseConfiguration"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      tags: [Bins]
      summary: Permanently delete a bin and its retained requests
      operationId: deleteBin
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/bins/{binId}/interactions:
    parameters:
      - $ref: "#/components/parameters/BinId"
    get:
      tags: [Requests]
      summary: List retained requests
      operationId: listRequests
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: Newest requests first
          content:
            application/json:
              schema:
                type: object
                required: [interactions]
                properties:
                  interactions:
                    type: array
                    items:
                      $ref: "#/components/schemas/CapturedRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/bins/{binId}/stream:
    parameters:
      - $ref: "#/components/parameters/BinId"
    get:
      tags: [Requests]
      summary: Stream request-created events
      operationId: streamRequests
      responses:
        "200":
          description: Server-sent events; each request event contains a request ID
          content:
            text/event-stream:
              schema:
                type: string
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/bins/{binId}/interactions/{requestId}:
    parameters:
      - $ref: "#/components/parameters/BinId"
      - $ref: "#/components/parameters/RequestId"
    delete:
      tags: [Requests]
      summary: Permanently delete one retained request
      operationId: deleteRequest
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/bins/{binId}/export:
    parameters:
      - $ref: "#/components/parameters/BinId"
    get:
      tags: [Requests]
      summary: Export a bin and all retained requests
      operationId: exportBin
      responses:
        "200":
          description: JSON download
          content:
            application/json:
              schema:
                type: object
                properties:
                  exportedAt:
                    type: string
                    format: date-time
                  bin:
                    $ref: "#/components/schemas/BinSummary"
                  requests:
                    type: array
                    items:
                      $ref: "#/components/schemas/CapturedRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/bins/{binId}/interactions/{requestId}/export:
    parameters:
      - $ref: "#/components/parameters/BinId"
      - $ref: "#/components/parameters/RequestId"
    get:
      tags: [Requests]
      summary: Export one retained request
      operationId: exportRequest
      responses:
        "200":
          description: JSON download
          content:
            application/json:
              schema:
                type: object
                properties:
                  exportedAt:
                    type: string
                    format: date-time
                  binId:
                    type: string
                  request:
                    $ref: "#/components/schemas/CapturedRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/bins/{binId}/interactions/{requestId}/replay:
    parameters:
      - $ref: "#/components/parameters/BinId"
      - $ref: "#/components/parameters/RequestId"
    post:
      tags: [Requests]
      summary: Replay one request to a public HTTPS URL
      operationId: replayRequest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  pattern: "^https://"
      responses:
        "200":
          description: Destination response summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  status:
                    type: integer
                  statusText:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/bins/{binId}/share:
    parameters:
      - $ref: "#/components/parameters/BinId"
    post:
      tags: [Sharing]
      summary: Enable, rotate, or disable a public bin link
      operationId: shareBin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ShareInput"
      responses:
        "200":
          $ref: "#/components/responses/Share"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/bins/{binId}/interactions/{requestId}/share:
    parameters:
      - $ref: "#/components/parameters/BinId"
      - $ref: "#/components/parameters/RequestId"
    post:
      tags: [Sharing]
      summary: Enable, rotate, or disable a public request link
      operationId: shareRequest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ShareInput"
      responses:
        "200":
          $ref: "#/components/responses/Share"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /b/{binId}/{path}:
    description: Every capture method requires a bearer API key belonging to the destination bin owner.
    parameters:
      - $ref: "#/components/parameters/BinId"
      - name: path
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Capture]
      summary: Capture a GET request
      operationId: captureGet
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Capture]
      summary: Capture a POST request
      operationId: capturePost
      description: The API key must belong to the owner of the destination bin.
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Request body exceeds the account limit
        "429":
          $ref: "#/components/responses/RateLimited"
    put:
      tags: [Capture]
      summary: Capture a PUT request
      operationId: capturePut
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Request body exceeds the account limit
        "429":
          $ref: "#/components/responses/RateLimited"
    patch:
      tags: [Capture]
      summary: Capture a PATCH request
      operationId: capturePatch
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Request body exceeds the account limit
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      tags: [Capture]
      summary: Capture a DELETE request
      operationId: captureDelete
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Request body exceeds the account limit
        "429":
          $ref: "#/components/responses/RateLimited"
    options:
      tags: [Capture]
      summary: Capture an OPTIONS request
      operationId: captureOptions
      responses:
        "200":
          description: Configured bin response
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          description: Request body exceeds the account limit
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: freebin API key
    cookieAuth:
      type: apiKey
      in: cookie
      name: freebin_session
  parameters:
    BinId:
      name: binId
      in: path
      required: true
      schema:
        type: string
    RequestId:
      name: requestId
      in: path
      required: true
      schema:
        type: string
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
    User:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
        email:
          type: string
          format: email
    RegisterInput:
      type: object
      required: [email, password, termsAccepted]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
          minLength: 10
          maxLength: 128
        termsAccepted:
          type: boolean
          const: true
    LoginInput:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
    PublicConfig:
      type: object
      required: [rumEndpoint, rumClientKey, signupsEnabled]
      properties:
        rumEndpoint:
          type: [string, "null"]
          format: uri
        rumClientKey:
          type: [string, "null"]
        signupsEnabled:
          type: boolean
    ApiKey:
      type: object
      required: [id, name, prefix, createdAt]
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
        createdAt:
          type: string
          format: date-time
        lastUsedAt:
          type: [string, "null"]
          format: date-time
    Account:
      allOf:
        - $ref: "#/components/schemas/User"
        - type: object
          properties:
            storageLimitBytes:
              type: integer
            storageUsedBytes:
              type: integer
    AccountResponse:
      type: object
      required: [user, bins]
      properties:
        user:
          oneOf:
            - $ref: "#/components/schemas/Account"
            - type: "null"
        bins:
          type: array
          items:
            $ref: "#/components/schemas/BinSummary"
    BinSummary:
      type: object
      required: [binId, name, createdAt]
      properties:
        binId:
          type: string
        name:
          type: string
        createdAt:
          type: string
          format: date-time
        interactionCount:
          type: integer
    Bin:
      allOf:
        - $ref: "#/components/schemas/BinSummary"
        - type: object
          properties:
            responseStatus:
              type: integer
            responseBody:
              type: string
            responseContentType:
              type: string
            responseHeaders:
              type: object
              additionalProperties:
                type: string
            publicShareToken:
              type: [string, "null"]
    CreateBinResponse:
      type: object
      required: [bin, inspectUrl]
      properties:
        bin:
          allOf:
            - $ref: "#/components/schemas/BinSummary"
            - type: object
              properties:
                url:
                  type: string
                  format: uri
        inspectUrl:
          type: string
          format: uri
    CapturedRequest:
      type: object
      required: [id, method, path, headers, query, timestamp, sizeBytes]
      properties:
        id:
          type: string
        method:
          type: string
        path:
          type: string
        query:
          type: object
          additionalProperties: true
        headers:
          type: object
          additionalProperties:
            type: string
        body:
          type: string
        contentType:
          type: [string, "null"]
        remoteAddress:
          type: [string, "null"]
        timestamp:
          type: string
          format: date-time
        sizeBytes:
          type: integer
        publicShareToken:
          type: [string, "null"]
    ResponseConfiguration:
      type: object
      properties:
        responseStatus:
          type: integer
          minimum: 100
          maximum: 599
        responseBody:
          type: string
          maxLength: 500
        responseContentType:
          type: string
          maxLength: 500
        responseHeaders:
          type: object
          maxProperties: 20
          additionalProperties:
            type: string
    ShareInput:
      type: object
      required: [public]
      properties:
        public:
          type: boolean
    ShareResponse:
      type: object
      properties:
        public:
          type: boolean
        shareUrl:
          type: [string, "null"]
          format: uri
  responses:
    UserSession:
      description: Authenticated user; a secure HttpOnly session cookie is set.
      headers:
        Set-Cookie:
          schema:
            type: string
      content:
        application/json:
          schema:
            type: object
            required: [user]
            properties:
              user:
                $ref: "#/components/schemas/User"
    Ok:
      description: Operation completed
      content:
        application/json:
          schema:
            type: object
            properties:
              ok:
                type: boolean
                const: true
    Share:
      description: Current public-link state
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ShareResponse"
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: The caller cannot manage this bin
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: Service limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Resource already exists or an account limit was reached
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: A required service or configuration is unavailable
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
