{
  "openapi": "3.1.0",
  "info": {
    "title": "TimeSpan API",
    "version": "1.0.0",
    "description": "Constraint-solver APIs for employee shift scheduling, task scheduling, field service routing, and pickup & delivery routing. Send your people, jobs and vehicles as JSON; get back a scored, feasible plan.\n\nAuthentication: pass `Authorization: Bearer ts_live_...` (create keys at Dashboard → API keys). API-key calls are stateless — the full dataset travels in the request body and nothing is read from or written to your stored data. Dashboard session-cookie auth is also accepted and runs statefully against your stored data.\n\nRate limits scale with your plan (Launch 5/min, Developer 15/min, Team 30/min, Enterprise 120/min; monthly solve caps apply). 429 responses include a Retry-After header; monthly quota exhaustion returns 402.",
    "contact": { "url": "https://www.timespan.online/dashboard/support" }
  },
  "servers": [{ "url": "https://www.timespan.online" }],
  "security": [{ "apiKey": [] }],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ts_live_...",
        "description": "TimeSpan API key from Dashboard → API keys."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, or revoked API key (and no session cookie).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "QuotaExceeded": {
        "description": "Monthly solve quota for your plan is exhausted.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Per-minute rate limit for your plan exceeded. Honor the Retry-After header.",
        "headers": { "Retry-After": { "schema": { "type": "string" }, "description": "Seconds to wait before retrying." } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BadRequest": {
        "description": "Malformed JSON or missing/invalid required fields.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } },
        "required": ["error"]
      },
      "Score": {
        "type": "object",
        "description": "Hard score must be 0 for a feasible plan; soft score is the optimization quality (closer to 0 is better).",
        "properties": { "hard": { "type": "integer" }, "soft": { "type": "integer" } }
      },
      "BreakdownItem": {
        "type": "object",
        "properties": {
          "code": { "type": "string", "description": "Constraint code, e.g. H1, S2." },
          "label": { "type": "string" },
          "severity": { "type": "string", "enum": ["hard", "soft"] },
          "count": { "type": "integer" },
          "impact": { "type": "integer" }
        }
      },
      "Employee": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "skills": { "type": "array", "items": { "type": "string" } },
          "max_shifts": { "type": "number", "default": 5 },
          "unavailable_days": { "type": "array", "items": { "type": "integer" }, "description": "0 = Monday … 6 = Sunday." }
        }
      },
      "Shift": {
        "type": "object",
        "required": ["id", "day", "start_hour", "end_hour"],
        "properties": {
          "id": { "type": "string" },
          "label": { "type": "string" },
          "day": { "type": "integer", "description": "0 = Monday … 6 = Sunday." },
          "start_hour": { "type": "number" },
          "end_hour": { "type": "number" },
          "required_skill": { "type": "string" }
        }
      },
      "SolveWeights": {
        "type": "object",
        "description": "Optional soft-constraint weights; omitted fields use platform defaults.",
        "additionalProperties": { "type": "number" }
      },
      "SolveRequest": {
        "type": "object",
        "required": ["employees", "shifts"],
        "properties": {
          "employees": { "type": "array", "items": { "$ref": "#/components/schemas/Employee" }, "minItems": 1 },
          "shifts": { "type": "array", "items": { "$ref": "#/components/schemas/Shift" }, "minItems": 1 },
          "weights": { "$ref": "#/components/schemas/SolveWeights" },
          "custom_constraints": { "type": "array", "items": { "type": "object" } },
          "pinned": { "type": "array", "items": { "type": "object" }, "description": "Assignments to lock in place before solving." }
        }
      },
      "SolveResponse": {
        "type": "object",
        "properties": {
          "mode": { "type": "string", "enum": ["stateless"] },
          "assignments": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": { "shift_id": { "type": "string" }, "employee_id": { "type": "string" } }
            }
          },
          "score": { "$ref": "#/components/schemas/Score" },
          "explanation": { "type": "array", "items": { "type": "string" } },
          "breakdown": { "type": "array", "items": { "$ref": "#/components/schemas/BreakdownItem" } },
          "metrics": { "type": "object", "additionalProperties": true }
        }
      },
      "TasksRequest": {
        "type": "object",
        "required": ["resources", "jobs"],
        "properties": {
          "resources": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "jobs": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "weights": { "$ref": "#/components/schemas/SolveWeights" }
        }
      },
      "FieldServiceRequest": {
        "type": "object",
        "required": ["technicians", "jobs"],
        "properties": {
          "technicians": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "jobs": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "weights": { "$ref": "#/components/schemas/SolveWeights" }
        }
      },
      "PickupDeliveryRequest": {
        "type": "object",
        "required": ["vehicles", "jobs"],
        "properties": {
          "vehicles": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "jobs": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true }, "minItems": 1 },
          "weights": { "$ref": "#/components/schemas/SolveWeights" }
        }
      },
      "RunResponse": {
        "type": "object",
        "properties": {
          "assignments": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
          "score": { "$ref": "#/components/schemas/Score" },
          "breakdown": { "type": "array", "items": { "$ref": "#/components/schemas/BreakdownItem" } },
          "metrics": { "type": "object", "additionalProperties": true }
        }
      },
      "CopilotRequest": {
        "type": "object",
        "required": ["question"],
        "properties": {
          "question": { "type": "string", "maxLength": 2000 },
          "context": { "type": "object", "description": "The score, constraint breakdown, and metrics from a solve response.", "additionalProperties": true }
        }
      },
      "CopilotResponse": {
        "type": "object",
        "properties": { "answer": { "type": "string" } }
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["operational", "degraded"] },
          "app": { "type": "string" },
          "database": { "type": "string" },
          "latency_ms": { "type": "integer" },
          "time": { "type": "string", "format": "date-time" }
        }
      }
    }
  },
  "paths": {
    "/api/solve": {
      "post": {
        "operationId": "solveShifts",
        "summary": "Employee shift scheduling",
        "description": "Assigns employees to shifts against skills, availability, overlap and fairness constraints.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveRequest" } } }
        },
        "responses": {
          "200": { "description": "Scored, feasible plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SolveResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/solve-tasks": {
      "post": {
        "operationId": "solveTasks",
        "summary": "Task scheduling",
        "description": "Assigns jobs to resources against skills, dependency ordering, and daily capacity.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TasksRequest" } } }
        },
        "responses": {
          "200": { "description": "Scored, feasible plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/solve-field-service": {
      "post": {
        "operationId": "solveFieldService",
        "summary": "Field service routing",
        "description": "Assigns and sequences site visits across technicians using drive-time estimates, time windows, and skill matching. Uses Google Distance Matrix when configured, straight-line estimates otherwise (flagged via metrics.distanceSource).",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FieldServiceRequest" } } }
        },
        "responses": {
          "200": { "description": "Scored, feasible routes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/solve-pickup-delivery": {
      "post": {
        "operationId": "solvePickupDelivery",
        "summary": "Pickup & delivery routing (VRPPD)",
        "description": "Routes vehicles through pickup/delivery stops respecting capacity, pickup-before-delivery precedence, and time windows.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PickupDeliveryRequest" } } }
        },
        "responses": {
          "200": { "description": "Scored, feasible routes.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RunResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/QuotaExceeded" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/copilot": {
      "post": {
        "operationId": "askCopilot",
        "summary": "Copilot — explain a solve result",
        "description": "Ask questions about a solve result in plain language. Grounded strictly in the context you pass. Returns 503 with Retry-After if the AI service is temporarily unavailable; solve endpoints are unaffected.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopilotRequest" } } }
        },
        "responses": {
          "200": { "description": "Copilot's answer.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CopilotResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "503": { "description": "Copilot temporarily unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Public health check",
        "security": [],
        "responses": {
          "200": { "description": "Platform health.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Health" } } } }
        }
      }
    }
  }
}
