openapi: 3.0.3
info:
  title: UluP Spaces API
  description: >
    REST API for UluP Spaces — create and manage projects, nodes, tasks and
    connections on your account. Every call acts on the data of the user
    who owns the key being used, with the same permissions that user would
    have in the app.


    To use it, generate a key from **Profile → API Keys** on
    [ulupspaces.com](https://www.ulupspaces.com), then pass it as the
    `Authorization: Bearer <key>` header on every request.


    To connect Claude directly (or any other MCP client), you don't need
    this API — use **Profile → Connect Claude** instead, which authenticates
    on its own via OAuth.
  version: "1.0.0"
  contact:
    name: UluP Studio
    url: https://ulupstudio.com

servers:
  - url: https://www.ulupspaces.com/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Projects
    description: Projects (workspaces) — the top-level container.
  - name: Nodes
    description: Nodes — the parts/phases of a project, and the connections between them.
  - name: Tasks
    description: Tasks inside a node.

paths:
  /projects:
    post:
      operationId: createProject
      summary: Create a new project
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  example: Podcast Launch
      responses:
        "201":
          description: Project created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Project"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /projects/{id}/nodes:
    parameters:
      - $ref: "#/components/parameters/ProjectId"
    get:
      operationId: listProjectNodes
      summary: List a project's nodes (structure only, not content)
      tags: [Nodes]
      responses:
        "200":
          description: List of nodes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/NodeStructure"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: createNode
      summary: Create a node in a project
      description: >
        Automatically checks for likely duplicates: if a node with an
        identical name already exists, it's reused instead of creating a
        new one (see `duplicateOf` in the response). If a node with a
        similar but not identical name exists, the new node is still
        created, with `similarTo` flagging it.
      tags: [Nodes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  example: Recording
                color:
                  type: string
                  enum: [blue, purple, green]
                  default: blue
      responses:
        "201":
          description: Node created (or reused, if an exact duplicate)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateNodeResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /projects/{id}/overview:
    parameters:
      - $ref: "#/components/parameters/ProjectId"
    get:
      operationId: getProjectOverview
      summary: Full current state of the project
      description: >
        Unlike `GET /projects/{id}/nodes`, this also includes the actual
        text of tasks (not just counts) and every connection between nodes.
        Use it when you need to read real content, not just structure.
      tags: [Projects]
      responses:
        "200":
          description: Full project state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectOverview"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /nodes/connect:
    post:
      operationId: connectNodes
      summary: Connect two existing nodes, by name
      description: >
        A connection is considered the same regardless of direction:
        connecting A→B when B→A already exists does not create a duplicate
        (see `alreadyConnected` in the response).
      tags: [Nodes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [project_id, source_node_name, target_node_name]
              properties:
                project_id:
                  type: integer
                  example: 293
                source_node_name:
                  type: string
                  example: Recording
                target_node_name:
                  type: string
                  example: Editing
      responses:
        "201":
          description: Connection created (or already existed)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Connection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /nodes/{id}/tasks:
    parameters:
      - $ref: "#/components/parameters/NodeId"
    post:
      operationId: addTask
      summary: Add a task to a node
      tags: [Tasks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  example: Record intro
      responses:
        "201":
          description: Task created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /tasks/{id}:
    parameters:
      - $ref: "#/components/parameters/TaskId"
    patch:
      operationId: completeTask
      summary: Mark a task complete or incomplete
      tags: [Tasks]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                completed:
                  type: boolean
                  default: true
                  description: If omitted, defaults to `true`.
      responses:
        "200":
          description: Updated status
          content:
            application/json:
              schema:
                type: object
                required: [completed]
                properties:
                  completed:
                    type: boolean
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Personal API key generated from Profile → API Keys on
        ulupspaces.com. It has full (read/write) access to the entire
        account of whoever generated it — don't share it.

  parameters:
    ProjectId:
      name: id
      in: path
      required: true
      description: Project id
      schema:
        type: integer
      example: 293
    NodeId:
      name: id
      in: path
      required: true
      description: Node id
      schema:
        type: integer
      example: 1263
    TaskId:
      name: id
      in: path
      required: true
      description: Task id
      schema:
        type: integer
      example: 1060

  schemas:
    Project:
      type: object
      required: [id, name]
      properties:
        id:
          type: integer
        name:
          type: string

    NodeStructure:
      type: object
      description: Structural node metadata — never the text of its tasks.
      required: [id, name, totalTasks, completedTasks]
      properties:
        id:
          type: integer
        name:
          type: string
        totalTasks:
          type: integer
        completedTasks:
          type: integer

    CreateNodeResult:
      type: object
      required: [id, name]
      properties:
        id:
          type: integer
        name:
          type: string
        duplicateOf:
          type: object
          nullable: true
          description: Present only if a node with an identical name already existed (reused instead of creating a new one).
          required: [id, name]
          properties:
            id:
              type: integer
            name:
              type: string
        similarTo:
          type: object
          nullable: true
          description: Present only if a node with a similar, non-identical name exists.
          required: [id, name]
          properties:
            id:
              type: integer
            name:
              type: string

    Connection:
      type: object
      required: [source, target]
      properties:
        source:
          type: string
        target:
          type: string
        alreadyConnected:
          type: boolean
          nullable: true

    Task:
      type: object
      required: [id, content]
      properties:
        id:
          type: integer
        content:
          type: string

    ProjectOverview:
      type: object
      required: [name, nodes, connections]
      properties:
        name:
          type: string
        nodes:
          type: array
          items:
            type: object
            required: [id, name, color, tasks]
            properties:
              id:
                type: integer
              name:
                type: string
              color:
                type: string
              tasks:
                type: array
                items:
                  type: object
                  required: [id, content, completed]
                  properties:
                    id:
                      type: integer
                    content:
                      type: string
                    completed:
                      type: boolean
        connections:
          type: array
          items:
            $ref: "#/components/schemas/Connection"

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string

  responses:
    BadRequest:
      description: Invalid request (missing or malformed parameter)
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing, invalid, or revoked API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found, or not owned by the key's user
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"