{
  "openapi": "3.1.0",
  "info": {
    "title": "Winnow API",
    "version": "0.1.0",
    "description": "Stateless API that scores the quality of a survey response and flags possible fraud (bots, speeders, straight-lining, duplicates). You send ONE response with its answers and timing metadata; it returns a quality score (0-100), a recommendation and the specific flags that fired. No AI/LLM: the logic is deterministic rules.\n"
  },
  "servers": [
    {
      "url": "https://api.licrat.com/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "BearerAuth": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "health",
        "summary": "Liveness check",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is alive",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/score": {
      "post": {
        "operationId": "scoreResponse",
        "summary": "Score a survey response for quality and fraud",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScoreRequest"
              },
              "examples": {
                "speeder": {
                  "summary": "A speeder who fails an attention check",
                  "value": {
                    "response_id": "resp-2024-0001",
                    "duration_seconds": 12,
                    "fingerprint": "9f86d081884c7d659a2feaa0c55ad015",
                    "survey": {
                      "total_questions": 4,
                      "min_expected_seconds": 60,
                      "attention_checks": [
                        {
                          "question_id": "ac1",
                          "expected_value": 3
                        }
                      ],
                      "grids": [
                        [
                          "g1",
                          "g2",
                          "g3",
                          "g4"
                        ]
                      ]
                    },
                    "answers": [
                      {
                        "question_id": "ac1",
                        "type": "scale",
                        "value": 5,
                        "seconds_spent": 3
                      },
                      {
                        "question_id": "g1",
                        "type": "grid",
                        "value": 1,
                        "seconds_spent": 3
                      },
                      {
                        "question_id": "g2",
                        "type": "grid",
                        "value": 1,
                        "seconds_spent": 3
                      },
                      {
                        "question_id": "g3",
                        "type": "grid",
                        "value": 1,
                        "seconds_spent": 3
                      },
                      {
                        "question_id": "g4",
                        "type": "grid",
                        "value": 1,
                        "seconds_spent": 3
                      },
                      {
                        "question_id": "o1",
                        "type": "open_text",
                        "value": "asdfghjkl",
                        "seconds_spent": 3
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Evaluation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScoreResult"
                },
                "examples": {
                  "flagged": {
                    "summary": "Response flagged for rejection",
                    "value": {
                      "response_id": "resp-2024-0001",
                      "quality_score": 0,
                      "recommendation": "reject",
                      "flags": [
                        {
                          "code": "speeding",
                          "severity": "high",
                          "detail": "Duration 12 s below the expected minimum of 60 s."
                        },
                        {
                          "code": "straight_lining",
                          "severity": "medium",
                          "detail": "Same option across all rows of 1 battery."
                        },
                        {
                          "code": "attention_check_failed",
                          "severity": "high",
                          "detail": "1 attention check failed: ac1."
                        },
                        {
                          "code": "uniform_timing",
                          "severity": "medium",
                          "detail": "Near-identical time (~3.00 s) on 5 of 5 questions."
                        }
                      ],
                      "checks_run": [
                        "speeding",
                        "straight_lining",
                        "attention_check_failed",
                        "duplicate",
                        "gibberish_open_text",
                        "uniform_timing"
                      ]
                    }
                  },
                  "clean": {
                    "summary": "Clean response",
                    "value": {
                      "response_id": "resp-2024-0002",
                      "quality_score": 100,
                      "recommendation": "accept",
                      "flags": [],
                      "checks_run": [
                        "speeding",
                        "straight_lining",
                        "attention_check_failed",
                        "duplicate",
                        "gibberish_open_text",
                        "uniform_timing"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "validation_error",
                  "message": "'response_id' is required and must be a non-empty string."
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid authentication token"
          },
          "413": {
            "description": "Body exceeds the maximum allowed size",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payload_too_large",
                  "message": "The body exceeds the maximum of 262144 bytes."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "ScoreRequest": {
        "type": "object",
        "required": [
          "response_id",
          "answers"
        ],
        "properties": {
          "response_id": {
            "type": "string",
            "description": "Your identifier for this response."
          },
          "duration_seconds": {
            "type": "number",
            "description": "Total time the respondent took, in seconds."
          },
          "fingerprint": {
            "type": "string",
            "description": "Hashed device/IP fingerprint (optional), used only to detect duplicates. Hash it on your side; never send raw PII.\n"
          },
          "survey": {
            "$ref": "#/components/schemas/SurveyMeta"
          },
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Answer"
            }
          }
        }
      },
      "SurveyMeta": {
        "type": "object",
        "description": "Optional hints about the questionnaire to fine-tune detection.",
        "properties": {
          "total_questions": {
            "type": "integer"
          },
          "min_expected_seconds": {
            "type": "number",
            "description": "Below this total time, the response is considered \"speeding\"."
          },
          "attention_checks": {
            "type": "array",
            "description": "Trap/control questions and their correct value.",
            "items": {
              "type": "object",
              "required": [
                "question_id",
                "expected_value"
              ],
              "properties": {
                "question_id": {
                  "type": "string"
                },
                "expected_value": {}
              }
            }
          },
          "grids": {
            "type": "array",
            "description": "Groups of question_id that form a battery/matrix (used to detect straight-lining).\n",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        }
      },
      "Answer": {
        "type": "object",
        "required": [
          "question_id",
          "type",
          "value"
        ],
        "properties": {
          "question_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "single",
              "multi",
              "scale",
              "grid",
              "open_text",
              "numeric"
            ]
          },
          "value": {
            "description": "The answer value (string, number, array, etc.)."
          },
          "seconds_spent": {
            "type": "number",
            "description": "Time spent on this question (optional), in seconds."
          }
        }
      },
      "ScoreResult": {
        "type": "object",
        "required": [
          "response_id",
          "quality_score",
          "recommendation",
          "flags"
        ],
        "properties": {
          "response_id": {
            "type": "string"
          },
          "quality_score": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "100 = clean, 0 = almost certainly fraudulent."
          },
          "recommendation": {
            "type": "string",
            "enum": [
              "accept",
              "review",
              "reject"
            ]
          },
          "flags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Flag"
            }
          },
          "checks_run": {
            "type": "array",
            "description": "Rules that were run on this response.",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Flag": {
        "type": "object",
        "required": [
          "code",
          "severity"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Codes emitted by v1: speeding, straight_lining, attention_check_failed, duplicate, gibberish_open_text, uniform_timing. `inconsistent_answers` is reserved and not emitted yet.\n",
            "enum": [
              "speeding",
              "straight_lining",
              "attention_check_failed",
              "duplicate",
              "gibberish_open_text",
              "inconsistent_answers",
              "uniform_timing"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation of why the flag fired."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}
