{
  "openapi": "3.1.0",
  "info": {
    "title": "LoopQuest API",
    "version": "1.0.0",
    "description": "Ingest automation output for human review and receive signed verdict webhooks."
  },
  "servers": [{ "url": "https://loopquest.tomphillips.uk" }],
  "security": [{ "bearer": [] }],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace API key (private queue) or the shared public-pool secret."
      }
    },
    "schemas": {
      "TaskInput": {
        "type": "object",
        "required": ["module", "payload"],
        "properties": {
          "module": { "type": "string", "enum": ["swiper", "versus", "sorter", "detective", "fixer", "redact", "grounding"], "description": "The review game." },
          "payload": { "type": "object", "description": "The data to review." },
          "mode": { "type": "string", "enum": ["gate", "monitor"], "default": "monitor", "description": "gate blocks a downstream action until a human decides; monitor reviews in the background." },
          "timeout_seconds": { "type": "integer", "minimum": 30, "maximum": 2592000, "description": "Gate only: apply on_timeout if no one reviews within this window." },
          "on_timeout": { "type": "string", "enum": ["escalate", "reject", "approve"], "default": "escalate", "description": "Gate only: what to do when the timeout is hit. Defaults to escalate (fail-closed)." },
          "source": { "type": "string", "description": "Originating engine / workflow id." },
          "callback_url": { "type": "string", "format": "uri", "description": "Where the verdict webhook is POSTed for this task." },
          "card": { "type": "object", "description": "Display hints: title, subtitle, suggestion, body, fields, choices." },
          "reviews_required": { "type": "integer", "minimum": 1, "maximum": 5, "description": "Consensus reviewers needed." },
          "golden": { "type": "boolean", "description": "Mark as a known-answer calibration task." },
          "answer": { "type": "boolean", "description": "The correct verdict, required when golden is true." },
          "external_id": { "type": "string", "description": "Your correlation id, echoed back in the verdict webhook." }
        }
      },
      "Subscription": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "event": { "type": "string", "enum": ["verdict"] }
        }
      },
      "TaskRef": {
        "type": "object",
        "properties": { "id": { "type": "string", "format": "uuid" }, "status": { "type": "string" } }
      },
      "TaskStatus": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["pending", "reviewed", "escalated"] },
          "verdict": { "type": ["boolean", "null"] },
          "verdict_choice": { "type": ["string", "null"] },
          "escalated": { "type": "boolean" },
          "timed_out": { "type": "boolean" },
          "reviewed_at": { "type": ["string", "null"], "format": "date-time" }
        }
      }
    }
  },
  "paths": {
    "/api/v1/me": {
      "get": {
        "summary": "Introspect the authenticated workspace",
        "description": "Validates a key and returns the workspace it maps to. Used by no-code connectors (Zapier, Make) as a connection test.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "org_id": { "type": ["string", "null"] }, "workspace": { "type": "string" } } } } } },
          "401": { "description": "Unauthorized" }
        }
      }
    },
    "/api/v1/tasks": {
      "post": {
        "summary": "Ingest a task for review",
        "parameters": [
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Repeat POSTs with the same key return the original task." }
        ],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskInput" } } } },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRef" } } } },
          "200": { "description": "Existing task returned (idempotent replay)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskRef" } } } },
          "401": { "description": "Unauthorized" },
          "422": { "description": "Validation error" },
          "429": { "description": "Rate limit exceeded (120 req / 60s per key)" }
        }
      }
    },
    "/api/v1/tasks/{id}": {
      "get": {
        "summary": "Get a task's status / verdict",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaskStatus" } } } },
          "401": { "description": "Unauthorized" },
          "404": { "description": "Not found (or outside your workspace)" }
        }
      }
    },
    "/api/v1/hooks": {
      "get": {
        "summary": "List verdict subscriptions",
        "description": "Every subscription for this workspace. Powers the instant 'New Verdict' triggers in no-code tools.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Subscription" } } } } },
          "401": { "description": "Unauthorized" }
        }
      },
      "post": {
        "summary": "Subscribe a URL to this workspace's verdicts",
        "description": "Idempotent by URL. Whenever any task in the workspace resolves, the verdict is POSTed (signed) to every subscribed URL.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["url"], "properties": { "url": { "type": "string", "format": "uri" } } } } } },
        "responses": {
          "201": { "description": "Subscribed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subscription" } } } },
          "200": { "description": "Existing subscription returned (idempotent)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Subscription" } } } },
          "401": { "description": "Unauthorized" },
          "422": { "description": "url must be an http(s) URL" }
        }
      }
    },
    "/api/v1/hooks/{id}": {
      "delete": {
        "summary": "Unsubscribe",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "204": { "description": "Unsubscribed" },
          "401": { "description": "Unauthorized" }
        }
      }
    }
  },
  "x-webhooks": {
    "verdict": {
      "post": {
        "description": "POSTed to callback_url when a task resolves. Signed with HMAC-SHA256 over the raw body in the X-LoopQuest-Signature header (sha256=…), verifiable with LOOPQUEST_WEBHOOK_SECRET.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": { "type": "string" },
                  "external_id": { "type": ["string", "null"] },
                  "source": { "type": ["string", "null"] },
                  "module": { "type": "string" },
                  "verdict": { "type": ["boolean", "null"], "description": "true = approved, false = flagged, null = escalated." },
                  "choice": { "type": ["string", "null"] },
                  "reason": { "type": ["string", "null"] },
                  "escalated": { "type": "boolean" },
                  "timed_out": { "type": "boolean" },
                  "reviewed_at": { "type": ["string", "null"], "format": "date-time" }
                }
              }
            }
          }
        }
      }
    }
  }
}
