{
  "openapi": "3.1.0",
  "info": {
    "title": "Taskr.dev API",
    "version": "1",
    "description": "**AI apps: use the MCP server instead.** Claude, ChatGPT, Claude Code, Codex and other MCP clients connect to `https://www.taskr.dev/mcp` by having the person sign in to Taskr.dev (OAuth): there's no token to copy. Setup steps for each app, for people: https://www.taskr.dev/ai.  This JSON API is for scripts and agents without MCP.\n\nTaskr.dev is a task manager built on relative priority: instead of P1/P2/P3 labels, every task is placed above or below other tasks in its owner's list, and the top task is what to work on now. Teammates in an organization can read and reorder each other's lists.\n\n**Authentication.** Send an agent token as `Authorization: Bearer taskr_...`. People create tokens on their profile page. A token acts as that person, locked to the organization they were in when they created it: it only reads and changes tasks there (or in Personal, for a token made there), and other tasks get 403. Browsers use the `session_id` cookie, set when a person signs in with Google, Microsoft, Apple or GitHub (there are no passwords). A `session_id` field in the JSON body also works; it is never read from the query string.\n\n**Organizations.** Everything is scoped to the organization the session is in, or Personal. Agent tokens can't switch organizations, create, rename or delete them, change membership or admin status, or sign the person out; those endpoints are marked browser-only and return 403 for tokens.\n\n**Requests.** GET parameters go in the query string (a JSON body also works). POST takes a JSON body with `Content-Type: application/json`. Paths also accept hyphens for underscores and a trailing slash.\n\n**Responses.** JSON. Actions with nothing to return give `{\"ok\": true}`; errors give `{\"error\": \"...\"}` with a 4xx or 5xx status.\n\n**Conventions.** Times are Unix timestamps in seconds; a deadline of 0 means none. Task descriptions and comments keep line breaks as ordinary newline characters. Lower priority numbers are more important, and only compare within one owner's list."
  },
  "externalDocs": {
    "description": "Use with AI: connecting Claude, ChatGPT and other AI apps (the MCP server)",
    "url": "https://www.taskr.dev/ai"
  },
  "servers": [
    {
      "url": "/"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "cookieAuth": []
    }
  ],
  "tags": [
    {
      "name": "Tasks"
    },
    {
      "name": "Prioritizing"
    },
    {
      "name": "Search"
    },
    {
      "name": "Account"
    },
    {
      "name": "Agent tokens"
    },
    {
      "name": "Organizations"
    },
    {
      "name": "Sign-in"
    }
  ],
  "paths": {
    "/api/create_task": {
      "post": {
        "operationId": "createTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Create a task",
        "description": "Creates an unprioritized task owned by you, in the organization this session is in (or Personal). Prioritize it afterwards with /api/prioritize_task.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "task_id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "task_id"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Defaults to the task ID if empty."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 100000,
                    "description": "Line breaks are kept as newline characters; \\r\\n and \\r are stored as \\n."
                  },
                  "estimate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Estimated effort in days, from 0 to 10000 (anything else is refused with 400). Default 0."
                  },
                  "deadline": {
                    "type": "string",
                    "description": "YYYY-MM-DD, from 2000-01-01 to 2999-12-31 (anything else is refused with 400), or empty for no deadline."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/get_task": {
      "get": {
        "operationId": "getTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Read a task and its comments",
        "description": "Returns one task you can access (yours, or any task in an organization you belong to), with its comments, newest first.",
        "parameters": [
          {
            "name": "task_id",
            "in": "query",
            "required": true,
            "description": "Task ID.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskWithComments"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/get_tasks": {
      "get": {
        "operationId": "getPrioritizedTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "List prioritized tasks",
        "description": "Open, prioritized tasks in the organization this session is in (or Personal), **most important first**. Despite the name, this is only the prioritized list; see also get_unprioritized_tasks and get_done_tasks.",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Optional. Another member's account ID, to read their list instead of yours. Both of you must be in the organization this session is in; otherwise 403.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Task"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/get_unprioritized_tasks": {
      "get": {
        "operationId": "getUnprioritizedTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "List unprioritized tasks",
        "description": "Open tasks that haven't been prioritized yet, oldest first.",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Optional. Another member's account ID, to read their list instead of yours. Both of you must be in the organization this session is in; otherwise 403.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Task"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/get_done_tasks": {
      "get": {
        "operationId": "getDoneTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "List done tasks",
        "description": "Closed tasks, most recently closed first. Without limit, all of them. The list only grows, so to read it a page at a time, pass limit, then pass the last task's id as after to get the page after it. A page with fewer than limit tasks is the last.",
        "parameters": [
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Optional. Another member's account ID, to read their list instead of yours. Both of you must be in the organization this session is in; otherwise 403.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Optional. At most this many tasks (1 to 1000). Without it, every done task.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Optional, with limit. The id of the last task of the previous page: the tasks closed before it. It must be a task in this list (the same owner and organization); a task reopened since still works.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Task"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid limit or after.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/update_task_details": {
      "post": {
        "operationId": "updateTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Update a task's details (and optionally its owner)",
        "description": "A partial update: only the fields you send change, and any of title, description, estimate or deadline that you leave out keeps its current value. An empty deadline removes the deadline, an empty description clears it, and an empty title is refused (400). Priority and done state are not affected. Setting owner (organization tasks only) hands the task to another member of its organization; an open task arrives unprioritized for its new owner.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string",
                    "description": "Task ID."
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 1000,
                    "description": "Optional. Can't be empty."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 100000,
                    "description": "Optional. Empty clears it."
                  },
                  "estimate": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Optional. Days, from 0 to 10000."
                  },
                  "deadline": {
                    "type": "string",
                    "description": "Optional. YYYY-MM-DD, from 2000-01-01 to 2999-12-31, or empty to remove the deadline."
                  },
                  "owner": {
                    "type": "string",
                    "description": "Optional. Account ID of the new owner; must be a member of the task's organization. Personal tasks can't be reassigned."
                  }
                },
                "required": [
                  "task_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/comment_task": {
      "post": {
        "operationId": "commentOnTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Comment on a task",
        "description": "Adds a comment as you. A comment that's empty or only whitespace is refused with 400; otherwise it's kept as written.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string",
                    "description": "Task ID."
                  },
                  "comment": {
                    "type": "string",
                    "maxLength": 10000
                  }
                },
                "required": [
                  "task_id",
                  "comment"
                ]
              }
            }
          }
        }
      }
    },
    "/api/edit_comment": {
      "post": {
        "operationId": "editComment",
        "tags": [
          "Tasks"
        ],
        "summary": "Edit one of your comments",
        "description": "Replaces the text of a comment you wrote, on a task you can reach (an agent token: only in its own organization). The comment keeps its place and time, and gets an edited timestamp. The same rule as commenting: empty or only whitespace is refused with 400. Someone else's comment is 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not your comment, or its task is out of reach (not a member, or outside an agent token's organization).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such comment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment_id": {
                    "type": "string",
                    "description": "Comment ID (the comment's key, as get_task returns it)."
                  },
                  "comment": {
                    "type": "string",
                    "maxLength": 10000
                  }
                },
                "required": [
                  "comment_id",
                  "comment"
                ]
              }
            }
          }
        }
      }
    },
    "/api/delete_comment": {
      "post": {
        "operationId": "deleteComment",
        "tags": [
          "Tasks"
        ],
        "summary": "Delete one of your comments",
        "description": "Deletes a comment you wrote, on a task you can reach (an agent token: only in its own organization). It's gone, with nothing left in its place. Someone else's comment is 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not your comment, or its task is out of reach (not a member, or outside an agent token's organization).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such comment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "comment_id": {
                    "type": "string",
                    "description": "Comment ID (the comment's key, as get_task returns it)."
                  }
                },
                "required": [
                  "comment_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/close_task": {
      "post": {
        "operationId": "closeTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Mark a task done",
        "description": "Moves the task to its owner's done list and out of search. Closing a task that's already done does nothing (and still succeeds): it keeps its place in the done list.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string",
                    "description": "Task ID."
                  }
                },
                "required": [
                  "task_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/reopen_task": {
      "post": {
        "operationId": "reopenOrUnprioritizeTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Reopen a task",
        "description": "Makes the task open and **unprioritized**. Use it to reopen a done task. It also takes an open task out of the prioritized list, but /api/unprioritize_task is the clearer way to do that.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string",
                    "description": "Task ID."
                  }
                },
                "required": [
                  "task_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/unprioritize_task": {
      "post": {
        "operationId": "unprioritizeTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Unprioritize a task",
        "description": "Takes an open task out of its owner's prioritized list; it becomes unprioritized. A task that is already unprioritized is left as it is. Done tasks are refused with 409; use /api/reopen_task to reopen one.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The task is done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string",
                    "description": "Task ID."
                  }
                },
                "required": [
                  "task_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/prioritize_task": {
      "post": {
        "operationId": "prioritizeTask",
        "tags": [
          "Prioritizing"
        ],
        "summary": "Place a task in its owner's prioritized list",
        "description": "Priority is relative: you place a task next to its neighbours in its **owner's** prioritized list (read the list with /api/get_tasks, most important first). All four fields are required; pass null where a case says so. `inserted_id` is the task being placed. Choose the case by where it goes:\n\n- **The list is empty:** `id1`, `id2` and `id3` are all null.\n- **Top (most important):** `id1` = the task being placed, `id2` = `id3` = the current top task.\n- **Bottom (least important):** `id1` = `id2` = the current bottom task, `id3` = the task being placed.\n- **Between two tasks:** `id1` = the task that should be just above it (more important), `id2` = the task being placed, `id3` = the task that should be just below it (less important). The two must be adjacent in the list.\n\nMoving a task that's already prioritized works the same way; leave it out when picking neighbours. The task becomes open and prioritized. The neighbours must be tasks this session can access too (403 otherwise).",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "inserted_id": {
                    "type": "string",
                    "description": "Task ID."
                  },
                  "id1": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "id2": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "id3": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "inserted_id",
                  "id1",
                  "id2",
                  "id3"
                ]
              }
            }
          }
        }
      }
    },
    "/api/search": {
      "get": {
        "operationId": "searchTasks",
        "tags": [
          "Search"
        ],
        "summary": "Search open tasks",
        "description": "Searches open tasks in the organization this session is in (or your Personal tasks), best match first. Matches whole words in titles (weighted more) and descriptions, case-insensitively; very common words are ignored.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search text, at most 1000 characters (longer is refused with 400).",
            "schema": {
              "type": "string",
              "maxLength": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Task"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Account"
        ],
        "summary": "Who am I",
        "description": "The account this session belongs to, the organization it's in (null for Personal), and whether it's an agent token.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/update_display_name": {
      "post": {
        "operationId": "updateDisplayName",
        "tags": [
          "Account"
        ],
        "summary": "Change your display name",
        "description": "Trimmed; 1–100 characters.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "display_name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "display_name"
                ]
              }
            }
          }
        }
      }
    },
    "/api/create_agent_token": {
      "post": {
        "operationId": "createAgentToken",
        "tags": [
          "Agent tokens"
        ],
        "summary": "Create an agent token",
        "description": "Creates a token that acts as you, locked to the organization this session is in. The token is only returned once. Tokens end when you sign out, when revoked, or when you stop being a member of the token's organization. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewAgentToken"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/agent_tokens": {
      "get": {
        "operationId": "listAgentTokens",
        "tags": [
          "Agent tokens"
        ],
        "summary": "List your agent tokens",
        "description": "Your agent tokens, oldest first, as your profile page lists them. Not the tokens themselves: those are only shown once, when made. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "token_id": {
                        "type": "string",
                        "description": "For /api/revoke_agent_token."
                      },
                      "name": {
                        "type": "string"
                      },
                      "org_id": {
                        "type": "string",
                        "description": "The organization the token works in, or empty for Personal."
                      },
                      "created": {
                        "type": "number",
                        "description": "Unix timestamp in seconds."
                      },
                      "last_used": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Unix timestamp in seconds, to within an hour; null if never used."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/connected_apps": {
      "get": {
        "operationId": "listConnectedApps",
        "tags": [
          "Agent tokens"
        ],
        "summary": "List your connected AI apps",
        "description": "AI apps such as Claude and ChatGPT that you've let use Taskr.dev as you by signing in with OAuth (/oauth/authorize), one per connection, oldest first, as your profile page lists them. **Browser sessions only:** agent tokens and apps' own tokens get 403.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "connection_id": {
                        "type": "string",
                        "description": "For /api/disconnect_app."
                      },
                      "client_name": {
                        "type": "string",
                        "description": "The app's name, as the app gives it."
                      },
                      "client_id": {
                        "type": "string",
                        "description": "How the app identifies itself: the URL of its metadata document, or the ID it registered."
                      },
                      "org_id": {
                        "type": "string",
                        "description": "The organization it works in, or empty for Personal."
                      },
                      "connected": {
                        "type": "number",
                        "description": "Unix timestamp in seconds."
                      },
                      "last_used": {
                        "type": "number",
                        "description": "Unix timestamp in seconds, to within an hour."
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/disconnect_app": {
      "post": {
        "operationId": "disconnectApp",
        "tags": [
          "Agent tokens"
        ],
        "summary": "Disconnect an AI app",
        "description": "Ends one connection: the app's access and refresh tokens stop working at once. **Browser sessions only.**",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "connection_id"
                ],
                "properties": {
                  "connection_id": {
                    "type": "string",
                    "description": "From /api/connected_apps."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Disconnected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such connected app.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/revoke_agent_token": {
      "post": {
        "operationId": "revokeAgentToken",
        "tags": [
          "Agent tokens"
        ],
        "summary": "Revoke one of your agent tokens",
        "description": "token_id comes from the token list on your profile page, or from create_agent_token. An agent token can revoke only itself, e.g. when its work is done; other token_ids get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "An agent token revoking a token other than itself.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "token_id"
                ]
              }
            }
          }
        }
      }
    },
    "/api/organizations": {
      "get": {
        "operationId": "listOrganizations",
        "tags": [
          "Organizations"
        ],
        "summary": "List your organizations",
        "description": "Organizations you belong to, by name, with your role in each.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OrganizationSummary"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/org": {
      "get": {
        "operationId": "getOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "The organization you're in",
        "description": "The organization this session is in: its members (by name, with roles) and its tasks. Without limit, all of them, open and done. With limit, its open tasks and only the newest limit done ones, as the website's organization page shows them; more_done says whether there are older done tasks, and /api/get_org_done_tasks reads them a page at a time. 404 in Personal.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Optional. At most this many done tasks (1 to 1000); every open task is always included.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrgView"
                }
              }
            }
          },
          "400": {
            "description": "Invalid limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/get_org_done_tasks": {
      "get": {
        "operationId": "getOrgDoneTasks",
        "tags": [
          "Organizations"
        ],
        "summary": "List the organization's done tasks",
        "description": "Everyone's closed tasks in the organization this session is in, most recently closed first, paged as /api/get_done_tasks: without limit, all of them; with limit, a page, and after the last task's id for the page after it. A page with fewer than limit tasks is the last. 404 in Personal.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Optional. At most this many tasks (1 to 1000). Without it, every done task.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          },
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "Optional, with limit. The id of the last task of the previous page: the tasks closed before it. It must be a task in this organization; a task reopened since still works.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Task"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid limit or after.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a member of the organization this session is in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "In Personal, which isn't an organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/invite": {
      "post": {
        "operationId": "createInvite",
        "tags": [
          "Organizations"
        ],
        "summary": "Create an invite link",
        "description": "Admins only. Creates a single-use invite to the organization this session is in, valid for 7 days. Agent tokens may use this. Give people the full URL: this site's origin followed by invite_path. Opening it in a browser shows which organization it's for and a Join button (after signing up or in, for someone signed out); they join by pressing it.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invite"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/create_organization": {
      "post": {
        "operationId": "createOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Create an organization",
        "description": "You become its first member and admin, and this session switches into it. Names needn't be unique. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "org_id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "org_id"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/set_active_org": {
      "post": {
        "operationId": "switchOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Switch organization",
        "description": "Switches this session into an organization you belong to, or into Personal with an empty org_id. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "org_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "org_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/join_organization": {
      "post": {
        "operationId": "joinOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Join an organization from an invite",
        "description": "The Join button on the page an invite link (/join/<invite_id>) shows. Joins the invite's organization, using up the invite, and switches this session into it. Someone already a member is just switched into it, and the invite stays unused. Opening the link alone never joins. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Joined (or already a member). Returns the organization's ID.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "org_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such invite (or already used), or its organization no longer exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The invite has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "invite_id": {
                    "type": "string",
                    "description": "The last part of the invite path, /join/<invite_id>."
                  }
                },
                "required": [
                  "invite_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/rename_organization": {
      "post": {
        "operationId": "renameOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Rename the organization you're in",
        "description": "Admins only. 1–100 characters. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 100
                  }
                },
                "required": [
                  "title"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/promote_admin": {
      "post": {
        "operationId": "promoteAdmin",
        "tags": [
          "Organizations"
        ],
        "summary": "Make a member an admin",
        "description": "Admins only, in the organization this session is in. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "user_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/remove_admin": {
      "post": {
        "operationId": "removeAdmin",
        "tags": [
          "Organizations"
        ],
        "summary": "Remove a member's admin status",
        "description": "Admins only. They stay a member. The last admin can't be removed (409). **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflicts with the current state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "user_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/remove_member": {
      "post": {
        "operationId": "removeMember",
        "tags": [
          "Organizations"
        ],
        "summary": "Remove a member",
        "description": "Admins only. Their tasks in the organization go to you, unprioritized. To remove yourself, use leave_organization. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "user_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "user_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/leave_organization": {
      "post": {
        "operationId": "leaveOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Leave an organization",
        "description": "Your tasks there go to an admin, unprioritized. The last admin can't leave (409). **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflicts with the current state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "org_id": {
                    "type": "string"
                  }
                },
                "required": [
                  "org_id"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/delete_organization": {
      "post": {
        "operationId": "deleteOrganization",
        "tags": [
          "Organizations"
        ],
        "summary": "Delete the organization you're in",
        "description": "Only the last admin, and only by passing its exact name. Deletes all of its tasks for everyone. **Browser sessions only:** agent tokens get 403.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflicts with the current state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm_name": {
                    "type": "string"
                  }
                },
                "required": [
                  "confirm_name"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/delete_account": {
      "post": {
        "operationId": "deleteAccount",
        "tags": [
          "Account"
        ],
        "summary": "Delete your account",
        "description": "Deletes the account and its Personal tasks, and ends every session and agent token. Its tasks in organizations go to an admin of each, as when leaving; comments it wrote on other tasks stay, with the author shown as \"Deleted user\". Refused with 409 while the account is the only admin of an organization. Pass confirm: \"DELETE\". **Browser sessions only:** agent tokens get 403. An account that signs in with Apple isn't deleted yet: the response is {\"next\": \"/auth/apple/delete\"}, where the browser goes to sign in with Apple once more, and the account is deleted (and Taskr.dev's access to the Apple Account revoked) when Apple sends it back. From the Taskr.dev app (which has no browser cookies), next is /auth/apple/delete?app_code=... with a one-time code, valid for 10 minutes, for the app to open in the browser; afterwards the browser goes to dev.taskr.app:/deleted (with ?error=cancelled if they cancelled).",
        "responses": {
          "200": {
            "description": "Done, or for an account that signs in with Apple, where the browser goes to finish.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Ok"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "next": {
                          "type": "string",
                          "description": "Where the browser goes to finish deleting an account that signs in with Apple."
                        }
                      },
                      "required": [
                        "next"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The account is the only admin of an organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "const": "DELETE"
                  }
                },
                "required": [
                  "confirm"
                ]
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/signout": {
      "post": {
        "operationId": "signOut",
        "tags": [
          "Sign-in"
        ],
        "summary": "Sign out everywhere",
        "description": "Ends **every** session for the account, including all agent tokens. **Browser sessions only:** agent tokens get 403; a token can end itself with revoke_agent_token.",
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "No session, or an invalid or expired one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed (not your task, not a member, not an admin, or an agent token where a browser session is required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-browser-only": true
      }
    },
    "/api/app_session": {
      "post": {
        "operationId": "appSession",
        "tags": [
          "Sign-in"
        ],
        "summary": "Finish signing in to the Taskr.dev app",
        "description": "For Taskr.dev's own phone apps, not agents. The app opens `/auth/{google|microsoft|apple|github}?app=1&code_challenge=...` in the phone's browser, where `code_challenge` is the base64url SHA-256 (no padding) of a random `code_verifier` the app keeps (PKCE, RFC 7636, S256). After the person signs in, the browser is sent to `dev.taskr.app:/signed-in?code=...` (or `?error=cancelled` if they cancelled). The app posts that code and its verifier here and gets a session, which it sends as `Authorization: Bearer <session_id>`. A code works once, for 5 minutes, and only with the verifier its challenge came from. App sessions can do everything a browser can, and end when the person signs out everywhere.",
        "security": [],
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "session_id": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "session_id"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing input, or a code that's wrong, used, expired or doesn't match the verifier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "code_verifier": {
                    "type": "string",
                    "minLength": 43,
                    "maxLength": 128,
                    "pattern": "^[A-Za-z0-9._~-]+$"
                  }
                },
                "required": [
                  "code",
                  "code_verifier"
                ]
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Line breaks are newline characters."
          },
          "owner": {
            "type": "string",
            "description": "Account ID of the task's owner."
          },
          "owner_name": {
            "type": "string",
            "description": "The owner's display name. In api/search results and api/get_task."
          },
          "organization": {
            "type": "string",
            "description": "Organization ID, or empty for a Personal task."
          },
          "org_key": {
            "type": "string",
            "description": "Internal index key; equals organization for organization tasks, absent for Personal ones."
          },
          "deadline": {
            "type": "number",
            "description": "Unix timestamp in seconds: midnight at the start of the deadline day, in the server's time zone. 0 means no deadline. Set from a YYYY-MM-DD date. To show it, use deadline_date."
          },
          "deadline_date": {
            "type": "string",
            "description": "The deadline as the date it was set to, YYYY-MM-DD, or empty for no deadline."
          },
          "estimate": {
            "type": "number",
            "description": "Estimated effort in days."
          },
          "created": {
            "type": "number",
            "description": "Unix timestamp in seconds (UTC)."
          },
          "unprioritized": {
            "type": "number",
            "description": "Unix timestamp in seconds (UTC). When the task last became unprioritized."
          },
          "resolved": {
            "type": "number",
            "description": "Unix timestamp in seconds (UTC). When it was closed; 0 if it hasn't been."
          },
          "prioritized": {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          },
          "done": {
            "type": "integer",
            "enum": [
              0,
              1
            ]
          },
          "priority": {
            "type": "number",
            "description": "Lower means more important. Only meaningful between prioritized tasks in the same owner's list; use the order of /api/get_tasks rather than the number."
          },
          "at_risk": {
            "type": "boolean",
            "description": "In task lists (get_tasks, get_unprioritized_tasks, get_done_tasks, search, org), not get_task. True if the task would be finished after its deadline day: a prioritized task once every task above it in its owner's list is done, any other open task if started now, each taking its estimate in working days (Monday to Friday). Done tasks and tasks without a deadline never are."
          }
        }
      },
      "Comment": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Comment ID (comment_id for api/edit_comment and api/delete_comment)."
          },
          "task": {
            "type": "string",
            "description": "Task ID."
          },
          "author": {
            "type": "string",
            "description": "Account ID of the author."
          },
          "author_name": {
            "type": "string",
            "description": "The author's display name now (\"Deleted user\" if their account is gone). In api/get_task."
          },
          "comment": {
            "type": "string",
            "description": "Line breaks are newline characters."
          },
          "timestamp": {
            "type": "number",
            "description": "Unix timestamp in seconds (UTC). When it was written; editing doesn't change it."
          },
          "edited": {
            "type": "number",
            "description": "Unix timestamp in seconds (UTC). When it was last edited; absent if it never was."
          }
        }
      },
      "TaskWithComments": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Task"
          },
          {
            "type": "object",
            "properties": {
              "comments": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Comment"
                },
                "description": "Newest first."
              },
              "owner_name": {
                "type": "string",
                "description": "The owner's display name."
              },
              "members": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "user_id": {
                      "type": "string"
                    },
                    "displayname": {
                      "type": "string"
                    }
                  }
                },
                "description": "Organization tasks only: the organization's members, by name, who the task can be handed to (owner in api/update_task_details)."
              }
            }
          }
        ]
      },
      "Me": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "displayname": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "active_org": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "read_only": {
                    "type": "boolean",
                    "description": "True while the organization's bill is unpaid: its tasks can be read, but creating or changing them is refused with 403."
                  },
                  "billing_notice": {
                    "oneOf": [
                      {
                        "type": "null"
                      },
                      {
                        "type": "object",
                        "properties": {
                          "text": {
                            "type": "string"
                          },
                          "for_admin": {
                            "type": "boolean",
                            "description": "Written for an admin, who can change the payment details (in Organization Settings' Billing section on the website)."
                          }
                        }
                      }
                    ],
                    "description": "The notice the website shows under its header while you're in this organization: payment details needed soon, an unpaid bill, or read-only.  null when there's nothing to say."
                  }
                }
              }
            ],
            "description": "The organization this session is in, or null for Personal."
          },
          "agent_token": {
            "type": "boolean",
            "description": "True when the request is using an agent token."
          },
          "provider": {
            "type": "string",
            "description": "Who the account signs in with: google, microsoft, apple or github."
          }
        }
      },
      "OrganizationSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ]
          }
        }
      },
      "Member": {
        "type": "object",
        "properties": {
          "user_id": {
            "type": "string"
          },
          "displayname": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ]
          }
        }
      },
      "OrgView": {
        "type": "object",
        "properties": {
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "expiration": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Unix timestamp in seconds (UTC). Paid through: the end of the free trial, then of the last month paid for."
              },
              "billing_status": {
                "type": "string",
                "enum": [
                  "trialing",
                  "active",
                  "past_due",
                  "unpaid",
                  "contract"
                ],
                "description": "trialing: the free trial. active: paid up. past_due: a bill failed and is being retried (on the 6th, 11th and 16th). unpaid: every retry failed, so the organization is read-only until a payment succeeds. contract: an Enterprise contract."
              },
              "trial_end": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Unix timestamp in seconds (UTC) when the free trial ends: the 1st of a month, when the first bill is due."
              },
              "read_only": {
                "type": "boolean",
                "description": "True while the organization's bill is unpaid: its tasks can be read, searched and exported, but creating or changing them is refused with 403."
              },
              "billing_summary": {
                "type": "string",
                "description": "Where the organization stands, as Organization Settings' Billing section says it, e.g. when the free trial ends."
              },
              "monthly_price": {
                "type": "number",
                "description": "What the organization's members cost a month now, in US dollars ($3 each for the first 50 members, $7 for each after that)."
              }
            }
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Member"
            }
          },
          "tasks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Task"
            },
            "description": "The organization's tasks: all of them, open and done; with limit, the open ones and the newest limit done ones."
          },
          "more_done": {
            "type": "boolean",
            "description": "With limit: whether there are older done tasks than these (read them with /api/get_org_done_tasks). Always false without limit."
          }
        }
      },
      "Invite": {
        "type": "object",
        "properties": {
          "invite_path": {
            "type": "string",
            "description": "Path of the invite link, e.g. /join/<id>. Prefix it with the site's origin."
          },
          "expires": {
            "type": "integer",
            "description": "Unix timestamp in seconds (UTC)."
          }
        }
      },
      "NewAgentToken": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "Starts with taskr_. Only shown here, once."
          },
          "token_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "ok"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          }
        },
        "required": [
          "error"
        ]
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An agent token (taskr_...), or a Taskr.dev app's session (from /api/app_session)."
      },
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session_id",
        "description": "Browser session cookie."
      }
    }
  }
}
