{
  "openapi": "3.0.3",
  "info": {
    "title": "DDO Quest Tracker Plugin API",
    "description": "HTTP API for the Dungeon Helper companion and other clients. Completions are keyed by quest remake (`difficulty`: Heroic | Epic | Legendary) and challenge `setting`. Many quests share a name across remakes (e.g. Haywire Foundry Heroic L9 and Epic L22) — use `/quests/search` or `/quests/lookup` to pick the correct row before posting completions.\n\nHuman docs: https://ddotracker.zepsu.com/api\nMarkdown: https://ddotracker.zepsu.com/api/plugin/docs",
    "version": "6.0.0",
    "contact": {
      "name": "DDO Quest Tracker",
      "url": "https://ddotracker.zepsu.com/companion"
    }
  },
  "servers": [
    {
      "url": "https://ddotracker.zepsu.com/api/plugin",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "Auth" },
    { "name": "Characters" },
    { "name": "Progress" },
    { "name": "Catalog" },
    { "name": "Routes" },
    { "name": "Live" },
    { "name": "Builder" },
    { "name": "System" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API token",
        "description": "Token from POST /auth/login (prefix typically ddot_…)"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" }
        },
        "required": ["error"]
      },
      "User": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "email": { "type": "string", "nullable": true },
          "username": { "type": "string", "nullable": true },
          "displayName": { "type": "string" }
        }
      },
      "RemakeDifficulty": {
        "type": "string",
        "enum": ["Heroic", "Epic", "Legendary", "Unknown"],
        "description": "Quest remake / content tier. Use Unknown when the companion cannot determine the remake."
      },
      "ChallengeSetting": {
        "type": "string",
        "enum": ["casual", "normal", "hard", "elite", "reaper", "unknown"],
        "description": "How the run was played. Use unknown when the SDK cannot detect challenge difficulty."
      },
      "QuestRow": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "example": "Haywire Foundry" },
          "difficulty": { "$ref": "#/components/schemas/RemakeDifficulty" },
          "level": { "type": "integer", "example": 9 },
          "soloXP": { "type": "integer" },
          "normalXP": { "type": "integer" },
          "hardXP": { "type": "integer" },
          "eliteXP": { "type": "integer" },
          "duration": { "type": "string" },
          "pack": { "type": "string" },
          "patron": { "type": "string" },
          "favor": { "type": "integer" }
        }
      },
      "Completion": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "difficulty": {
            "$ref": "#/components/schemas/RemakeDifficulty",
            "description": "Optional. Prefer the remake that was run; send Unknown when unsure. Completions are never rejected for bad/missing difficulty."
          },
          "setting": { "$ref": "#/components/schemas/ChallengeSetting" },
          "completedAt": { "type": "string", "format": "date-time" },
          "durationSeconds": { "type": "integer", "nullable": true },
          "xpEarned": {
            "type": "integer",
            "nullable": true,
            "description": "Actual XP awarded in-game. Preferred for stats; when omitted the website estimates from the catalog."
          },
          "reaperXpEarned": {
            "type": "integer",
            "nullable": true,
            "description": "Reaper experience (RXP) from a Reaper-difficulty run. Shown on completed quests when present. Aliases: reaperXp, rxp."
          }
        },
        "required": ["name"]
      }
    }
  },
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/auth/login": {
      "post": {
        "tags": ["Auth"],
        "security": [],
        "summary": "Issue API token",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["password"],
                "properties": {
                  "identifier": { "type": "string", "description": "Username or email (preferred)" },
                  "email": { "type": "string", "description": "Alias for identifier" },
                  "username": { "type": "string", "description": "Alias for identifier" },
                  "login": { "type": "string", "description": "Alias for identifier" },
                  "password": { "type": "string" },
                  "label": { "type": "string", "example": "dungeonhelper" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": { "type": "string" },
                    "tokenId": { "type": "integer" },
                    "label": { "type": "string" },
                    "user": { "$ref": "#/components/schemas/User" }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["Auth"],
        "summary": "Current token owner",
        "responses": {
          "200": {
            "description": "User profile",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } }
          }
        }
      }
    },
    "/auth/logout": {
      "post": {
        "tags": ["Auth"],
        "summary": "Revoke current bearer token",
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "ok": { "type": "boolean" } }
                }
              }
            }
          }
        }
      }
    },
    "/characters": {
      "get": {
        "tags": ["Characters"],
        "summary": "List characters",
        "responses": { "200": { "description": "Character profiles" } }
      },
      "post": {
        "tags": ["Characters"],
        "summary": "Create character",
        "responses": { "201": { "description": "Created" } }
      }
    },
    "/characters/meta": {
      "get": {
        "tags": ["Characters"],
        "summary": "Picklists (races, classes, servers, …)",
        "responses": { "200": { "description": "Meta" } }
      }
    },
    "/characters/find": {
      "get": {
        "tags": ["Characters"],
        "summary": "Find character by in-game name / server",
        "parameters": [
          { "name": "givenName", "in": "query", "schema": { "type": "string" } },
          { "name": "surname", "in": "query", "schema": { "type": "string" } },
          { "name": "name", "in": "query", "schema": { "type": "string" } },
          { "name": "server", "in": "query", "schema": { "type": "string" } }
        ],
        "responses": { "200": { "description": "Matches" } }
      }
    },
    "/characters/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
      ],
      "get": {
        "tags": ["Characters"],
        "summary": "Get character",
        "responses": { "200": { "description": "Profile" } }
      },
      "patch": {
        "tags": ["Characters"],
        "summary": "Update character (partial)",
        "responses": { "200": { "description": "Updated" } }
      },
      "put": {
        "tags": ["Characters"],
        "summary": "Update character (same as PATCH)",
        "responses": { "200": { "description": "Updated" } }
      }
    },
    "/characters/{id}/login": {
      "post": {
        "tags": ["Characters"],
        "summary": "Heartbeat last login",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Updated" } }
      }
    },
    "/characters/{id}/progress": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
      ],
      "get": {
        "tags": ["Progress"],
        "summary": "Get tracker progress (filters + completions)",
        "responses": { "200": { "description": "Progress payload" } }
      },
      "put": {
        "tags": ["Progress"],
        "summary": "Update progress (partial)",
        "responses": { "200": { "description": "Updated progress" } }
      }
    },
    "/characters/{id}/completions": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
      ],
      "post": {
        "tags": ["Progress"],
        "summary": "Upsert one completion",
        "description": "Prefer the exact remake `difficulty` from the catalog (use /quests/search when the quest has Heroic and Epic copies). When remake or challenge is unknown, still POST — send `difficulty: Unknown` and/or `setting: unknown`. Include `xpEarned` with the actual XP from the game when available; the website falls back to catalog estimates otherwise. On Reaper runs, also send `reaperXpEarned` (RXP) when available.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/Completion" },
              "example": {
                "name": "Haywire Foundry",
                "difficulty": "Heroic",
                "setting": "reaper",
                "durationSeconds": 600,
                "xpEarned": 12840,
                "reaperXpEarned": 1840
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updated existing" },
          "201": { "description": "Created" }
        }
      },
      "delete": {
        "tags": ["Progress"],
        "summary": "Remove completion(s)",
        "description": "Pass allSettings:true or setting=all to clear every challenge for that remake.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "difficulty": { "$ref": "#/components/schemas/RemakeDifficulty" },
                  "setting": { "type": "string" },
                  "allSettings": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": { "200": { "description": "Removed" } }
      }
    },
    "/quests": {
      "get": {
        "tags": ["Catalog"],
        "summary": "Full quest catalog",
        "description": "One object per remake row (Heroic / Epic / Legendary are separate).",
        "responses": {
          "200": {
            "description": "Catalog",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": { "type": "integer" },
                    "quests": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/QuestRow" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quests/search": {
      "get": {
        "tags": ["Catalog"],
        "summary": "Quest autocomplete (all remakes)",
        "description": "Never collapses families — Haywire Foundry returns both Heroic L9 and Epic L22.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "Substring on name (also pack/patron). Aliases: query, name"
          },
          {
            "name": "difficulty",
            "in": "query",
            "schema": { "$ref": "#/components/schemas/RemakeDifficulty" }
          },
          {
            "name": "level",
            "in": "query",
            "schema": { "type": "integer" },
            "description": "Character level — ranks enterable / closest remakes first. Aliases: characterLevel, cl"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked matches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": { "type": "string" },
                    "count": { "type": "integer" },
                    "totalMatches": { "type": "integer" },
                    "level": { "type": "integer", "nullable": true },
                    "quests": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          { "$ref": "#/components/schemas/QuestRow" },
                          {
                            "type": "object",
                            "properties": {
                              "score": { "type": "number" },
                              "enterable": { "type": "boolean", "nullable": true }
                            }
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/quests/lookup": {
      "get": {
        "tags": ["Catalog"],
        "summary": "Exact quest name lookup",
        "parameters": [
          { "name": "name", "in": "query", "required": true, "schema": { "type": "string" } },
          {
            "name": "difficulty",
            "in": "query",
            "schema": { "$ref": "#/components/schemas/RemakeDifficulty" }
          }
        ],
        "responses": {
          "200": {
            "description": "Match(es) plus remakes list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quests": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/QuestRow" }
                    },
                    "remakes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": { "type": "string" },
                          "difficulty": { "$ref": "#/components/schemas/RemakeDifficulty" },
                          "level": { "type": "integer" },
                          "pack": { "type": "string" },
                          "patron": { "type": "string" }
                        }
                      }
                    },
                    "remakeCount": { "type": "integer" }
                  }
                }
              }
            }
          },
          "404": { "description": "Not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/scorecards": {
      "get": {
        "tags": ["Catalog"],
        "summary": "Quest scorecard thresholds",
        "responses": { "200": { "description": "Scorecards" } }
      }
    },
    "/routes": {
      "get": {
        "tags": ["Routes"],
        "summary": "List shared routes",
        "responses": { "200": { "description": "Routes" } }
      }
    },
    "/routes/{id}": {
      "get": {
        "tags": ["Routes"],
        "summary": "Route detail",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Route with ordered quests" } }
      }
    },
    "/navigation": {
      "get": {
        "tags": ["Routes"],
        "summary": "Route progress for a character",
        "parameters": [
          { "name": "characterId", "in": "query", "required": true, "schema": { "type": "integer" } },
          { "name": "routeId", "in": "query", "required": true, "schema": { "type": "integer" } },
          {
            "name": "match",
            "in": "query",
            "schema": { "type": "string", "enum": ["setting", "remake"], "default": "setting" }
          }
        ],
        "responses": { "200": { "description": "Steps + nextQuest" } }
      }
    },
    "/info": {
      "get": {
        "tags": ["System"],
        "summary": "API version and feature flags",
        "responses": {
          "200": {
            "description": "Info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "apiVersion": { "type": "integer", "example": 3 },
                    "questCatalogCount": { "type": "integer" },
                    "docs": { "type": "string", "example": "/api/plugin/docs" },
                    "openapi": { "type": "string", "example": "/api/openapi.json" },
                    "features": { "type": "object", "additionalProperties": true }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/characters/{id}/live": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
      ],
      "get": {
        "tags": ["Live"],
        "summary": "Get live in-game character snapshot (companion)",
        "description": "What the character is right now. Separate from the planned /build document.",
        "responses": {
          "200": { "description": "Live snapshot" },
          "404": { "description": "No live snapshot yet" }
        }
      },
      "put": {
        "tags": ["Live"],
        "summary": "Replace live snapshot (or merge with ?merge=1)",
        "description": "Writes the companion sheet. Syncs quest-tracker profile (classes, level, race, past lives). Does not overwrite planned /build.",
        "parameters": [
          {
            "name": "merge",
            "in": "query",
            "schema": { "type": "boolean" },
            "description": "If true, merge into existing live snapshot instead of replacing"
          }
        ],
        "responses": { "200": { "description": "Saved" } }
      }
    },
    "/characters/{id}/live/partial": {
      "put": {
        "tags": ["Live"],
        "summary": "Merge live snapshot patch",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Saved" } }
      }
    },
    "/characters/{id}/live/promote-to-planned": {
      "post": {
        "tags": ["Live"],
        "summary": "Copy live snapshot into planned builder build",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Planned build updated from live" },
          "404": { "description": "No live snapshot" }
        }
      }
    },
    "/characters/{id}/build": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
      ],
      "get": {
        "tags": ["Builder"],
        "summary": "Get planned build document",
        "description": "What you plan to be in the character builder. Independent of /live.",
        "responses": { "200": { "description": "Planned build" } }
      },
      "put": {
        "tags": ["Builder"],
        "summary": "Replace planned build document",
        "responses": { "200": { "description": "Saved" } }
      }
    },
    "/characters/{id}/build/partial": {
      "put": {
        "tags": ["Builder"],
        "summary": "Merge planned build patch",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Saved" } }
      }
    },
    "/characters/{id}/lives": {
      "get": {
        "tags": ["Builder"],
        "summary": "List character lives",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "Lives" } }
      }
    },
    "/characters/{id}/lives/reincarnate": {
      "post": {
        "tags": ["Builder"],
        "summary": "Reincarnate (archive life, open new)",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": { "200": { "description": "New life" } }
      }
    }
  }
}
