{
  "openapi": "3.1.0",
  "info": {
    "title": "LessRounds API",
    "version": "1",
    "description": "Read your LessRounds interviews and candidates from your own tools. Create an API key in LessRounds under Settings → API & webhooks and send it as \"Authorization: Bearer lr_live_…\". We may add fields and endpoints at any time; ignore what you don't know. Anything removed or renamed means a new version (/v2), announced 90 days ahead. Guides: https://lessrounds.ai/developers",
    "contact": {
      "email": "support@lessrounds.ai"
    }
  },
  "servers": [
    {
      "url": "https://api.lessrounds.ai/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Check your key",
        "description": "Returns your company and this key's name. Use it to test a connection.",
        "responses": {
          "200": {
            "description": "Your company and key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Me"
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/interviews": {
      "get": {
        "operationId": "listInterviews",
        "summary": "List interviews",
        "description": "Newest first. Deleted interviews are never listed.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only interviews with this status.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "active",
                "paused",
                "closed"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many to return (1–100, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The next_cursor of the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of interviews.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "interviews",
                        "next_cursor"
                      ],
                      "properties": {
                        "interviews": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Interview"
                          }
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Pass as cursor to get the next page; null on the last page."
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "422": {
            "$ref": "#/components/responses/BadQuery"
          }
        }
      }
    },
    "/interviews/{id}": {
      "get": {
        "operationId": "getInterview",
        "summary": "Get an interview",
        "description": "One interview with its steps: form questions (their ids are the keys of a candidate's answers), the AI interview and the document asked for.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The interview.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "interview"
                      ],
                      "properties": {
                        "interview": {
                          "$ref": "#/components/schemas/Interview"
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/candidates": {
      "get": {
        "operationId": "listCandidates",
        "summary": "List candidates",
        "description": "Candidate attempts as full candidate objects. By default only attempts the candidate submitted, most recently submitted first. With state=in_progress or state=all, newest started first. The order never changes while you page, so paging with next_cursor never skips anyone. Team members' previews are never listed.",
        "parameters": [
          {
            "name": "interview_id",
            "in": "query",
            "required": false,
            "description": "Only this interview's candidates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One or more statuses, separated by commas: in_progress, passed, needs_review, did_not_pass, shortlisted, on_hold, rejected, cancelled, abandoned.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "submitted (default): finished every step. in_progress: still taking it. all: everything, including cancelled and unfinished attempts.",
            "schema": {
              "type": "string",
              "enum": [
                "submitted",
                "in_progress",
                "all"
              ]
            }
          },
          {
            "name": "submitted_after",
            "in": "query",
            "required": false,
            "description": "Only candidates who submitted at or after this time (RFC 3339).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "submitted_before",
            "in": "query",
            "required": false,
            "description": "Only candidates who submitted before this time (RFC 3339).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many to return (1–25, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "The next_cursor of the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of candidates.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "candidates",
                        "next_cursor"
                      ],
                      "properties": {
                        "candidates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Candidate"
                          }
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Pass as cursor to get the next page; null on the last page."
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "422": {
            "$ref": "#/components/responses/BadQuery"
          }
        }
      }
    },
    "/candidates/{id}": {
      "get": {
        "operationId": "getCandidate",
        "summary": "Get a candidate",
        "description": "One candidate attempt, in any state.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The candidate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "candidate"
                      ],
                      "properties": {
                        "candidate": {
                          "$ref": "#/components/schemas/Candidate"
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/candidates/{id}/transcript": {
      "get": {
        "operationId": "getTranscript",
        "summary": "Get the interview transcript",
        "description": "The AI interview, question by question. [] when there's no AI interview yet.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "transcript"
                      ],
                      "properties": {
                        "transcript": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TranscriptEntry"
                          }
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/candidates/{id}/recording": {
      "get": {
        "operationId": "getRecording",
        "summary": "Get a download link for the interview recording",
        "description": "Returns a link that works for 15 minutes. 404 when there's no recording.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A download link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "recording"
                      ],
                      "properties": {
                        "recording": {
                          "$ref": "#/components/schemas/RecordingDownload"
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "Get the change history",
        "description": "What happened to your candidates, oldest first, for the last 30 days: candidate.submitted, candidate.scored, candidate.ready and candidate.status_changed. Save next_cursor and pass it back as after next time to get only what's new — it's returned even when nothing happened, and has_more says whether to ask again straight away. An event usually appears here within seconds of happening, and always within about a minute.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "required": false,
            "description": "The next_cursor of your previous call. Leave out to start from the oldest kept event.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "One or more event types, separated by commas.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "interview_id",
            "in": "query",
            "required": false,
            "description": "Only this interview's candidates.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many to return (1–25, default 25).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "events",
                        "next_cursor",
                        "has_more"
                      ],
                      "properties": {
                        "events": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Event"
                          }
                        },
                        "next_cursor": {
                          "type": "string",
                          "description": "Pass as after next time."
                        },
                        "has_more": {
                          "type": "boolean",
                          "description": "More events are waiting right now."
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "422": {
            "$ref": "#/components/responses/BadQuery"
          }
        }
      }
    },
    "/files/{id}": {
      "get": {
        "operationId": "getFile",
        "summary": "Get a download link for a candidate's document",
        "description": "For files in a candidate's documents list. Returns a link that works for 15 minutes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A download link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "file"
                      ],
                      "properties": {
                        "file": {
                          "$ref": "#/components/schemas/FileDownload"
                        }
                      },
                      "additionalProperties": false
                    },
                    "timestamp": {
                      "type": "integer"
                    },
                    "trace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Suspended"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "webhooks": {
    "candidate.submitted": {
      "post": {
        "summary": "A candidate finished the interview",
        "description": "Sent once per attempt, when the candidate has finished every step. The body is the event with the candidate’s latest data. Answer with any 2xx within 10 seconds; anything else is retried for about 3 days (after 1, 5 and 30 minutes, then 2, 6, 12, 24 and 24 hours). Messages about one candidate arrive in the order they happened.",
        "parameters": [
          {
            "name": "LessRounds-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id (the same as the body’s id). Stays the same on every retry: use it to ignore a message you already handled."
          },
          {
            "name": "LessRounds-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event type, the same as the body’s type."
          },
          {
            "name": "LessRounds-Delivery-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "1 for the first try, 2 for the first retry, and so on (at most 9)."
          },
          {
            "name": "LessRounds-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook’s signing secret>. For 24 hours after you replace the secret there are two v1 values, one per secret. Checking it is optional."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx counts; redirects are not followed and count as a failure."
          }
        }
      }
    },
    "candidate.scored": {
      "post": {
        "summary": "The AI scored the interview",
        "description": "Sent once per attempt, when the AI score is saved (never before candidate.submitted). Not sent when the interview couldn’t be scored. The body is the event with the candidate’s latest data. Answer with any 2xx within 10 seconds; anything else is retried for about 3 days (after 1, 5 and 30 minutes, then 2, 6, 12, 24 and 24 hours). Messages about one candidate arrive in the order they happened.",
        "parameters": [
          {
            "name": "LessRounds-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id (the same as the body’s id). Stays the same on every retry: use it to ignore a message you already handled."
          },
          {
            "name": "LessRounds-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event type, the same as the body’s type."
          },
          {
            "name": "LessRounds-Delivery-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "1 for the first try, 2 for the first retry, and so on (at most 9)."
          },
          {
            "name": "LessRounds-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook’s signing secret>. For 24 hours after you replace the secret there are two v1 values, one per secret. Checking it is optional."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx counts; redirects are not followed and count as a failure."
          }
        }
      }
    },
    "candidate.ready": {
      "post": {
        "summary": "The result is final",
        "description": "Sent once per attempt, when the result is final: complete (score and recording), incomplete (result.incomplete_reason says why) or not_applicable (no AI step). Nothing more arrives for this attempt except status changes. The body is the event with the candidate’s latest data. Answer with any 2xx within 10 seconds; anything else is retried for about 3 days (after 1, 5 and 30 minutes, then 2, 6, 12, 24 and 24 hours). Messages about one candidate arrive in the order they happened.",
        "parameters": [
          {
            "name": "LessRounds-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id (the same as the body’s id). Stays the same on every retry: use it to ignore a message you already handled."
          },
          {
            "name": "LessRounds-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event type, the same as the body’s type."
          },
          {
            "name": "LessRounds-Delivery-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "1 for the first try, 2 for the first retry, and so on (at most 9)."
          },
          {
            "name": "LessRounds-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook’s signing secret>. For 24 hours after you replace the secret there are two v1 values, one per secret. Checking it is optional."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx counts; redirects are not followed and count as a failure."
          }
        }
      }
    },
    "candidate.status_changed": {
      "post": {
        "summary": "The candidate’s status changed",
        "description": "Sent on every status change after submission, by the AI or your team. change says from what, to what, and who made it. The body is the event with the candidate’s latest data. Answer with any 2xx within 10 seconds; anything else is retried for about 3 days (after 1, 5 and 30 minutes, then 2, 6, 12, 24 and 24 hours). Messages about one candidate arrive in the order they happened.",
        "parameters": [
          {
            "name": "LessRounds-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id (the same as the body’s id). Stays the same on every retry: use it to ignore a message you already handled."
          },
          {
            "name": "LessRounds-Event-Type",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event type, the same as the body’s type."
          },
          {
            "name": "LessRounds-Delivery-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "1 for the first try, 2 for the first retry, and so on (at most 9)."
          },
          {
            "name": "LessRounds-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"<t>.<raw body>\" with your webhook’s signing secret>. For 24 hours after you replace the secret there are two v1 values, one per secret. Checking it is optional."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received. Any 2xx counts; redirects are not followed and count as a failure."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your LessRounds API key (lr_live_…)."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The key is missing, not valid or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Suspended": {
        "description": "Your company account is suspended.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests: at most 120 a minute per key and 300 per company. Wait for the time in the Retry-After header.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The API is read-only: only GET is accepted.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Nothing with this id in your company.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadQuery": {
        "description": "A query parameter isn't valid; details.problems lists each one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error",
          "message"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "error": {
            "type": "string",
            "description": "Error code: validation_error, not_found, auth_error, permission_denied, rate_limited, external_service_error, internal_error."
          },
          "message": {
            "type": "string",
            "description": "A plain-language explanation you can show to a person."
          },
          "details": {
            "description": "Extra detail for some errors, e.g. the list of problems with a query."
          },
          "timestamp": {
            "type": "integer"
          },
          "trace_id": {
            "type": "string",
            "description": "Quote this when you contact support."
          }
        }
      },
      "Me": {
        "type": "object",
        "required": [
          "company",
          "api_key"
        ],
        "properties": {
          "company": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "additionalProperties": false
          },
          "api_key": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "additionalProperties": false
          }
        },
        "additionalProperties": false
      },
      "InterviewRef": {
        "type": "object",
        "required": [
          "id",
          "title"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "Highlight": {
        "type": "object",
        "required": [
          "quote",
          "type",
          "why"
        ],
        "properties": {
          "quote": {
            "type": "string",
            "description": "The candidate's own words."
          },
          "type": {
            "type": "string",
            "enum": [
              "strength",
              "concern",
              "insight",
              "conduct"
            ]
          },
          "why": {
            "type": "string",
            "description": "One sentence on why it matters."
          }
        },
        "additionalProperties": false
      },
      "Result": {
        "type": "object",
        "required": [
          "state",
          "incomplete_reason",
          "score",
          "recommendation",
          "recommendation_label",
          "pass_mark",
          "summary",
          "strengths",
          "areas_to_improve",
          "skills",
          "highlights",
          "ended_for_conduct"
        ],
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "waiting",
              "complete",
              "incomplete",
              "not_applicable"
            ],
            "description": "waiting: not final yet (still taking it, or the score or recording is on its way). complete: the AI score and recording are in (the same moment credits are charged) — this can happen before the candidate finishes a later step. incomplete: final, but something is missing (see incomplete_reason). not_applicable: the interview has no AI step."
          },
          "incomplete_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "not_submitted: the attempt ended without being submitted (cancelled, left unfinished, or moved on by your team).",
            "enum": [
              "not_submitted",
              "interview_not_finished",
              "too_little_speech",
              "no_answers",
              "scoring_failed",
              "recording_failed",
              "not_delivered_in_time",
              "other",
              null
            ]
          },
          "score": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "The AI score, 0–100."
          },
          "recommendation": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "strongly_recommend",
              "recommended",
              "consider",
              "not_recommended",
              null
            ]
          },
          "recommendation_label": {
            "type": [
              "string",
              "null"
            ]
          },
          "pass_mark": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The interview's pass mark, if it has one."
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "strengths": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "areas_to_improve": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "skills": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Skill name → score (0–100)."
          },
          "highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Highlight"
            }
          },
          "ended_for_conduct": {
            "type": "boolean",
            "description": "The AI interview was ended for repeated conduct violations."
          }
        },
        "additionalProperties": false
      },
      "Recording": {
        "type": "object",
        "required": [
          "status",
          "url"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed",
              "none",
              "deleted"
            ],
            "description": "processing: on its way (it can arrive up to about two days after the interview, even when the result is already final). failed: it didn't arrive or couldn't be processed. none: the candidate's device recorded nothing. deleted: removed after the retention period."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "API link (send your key) that returns a download link. Null when there's nothing to download."
          }
        },
        "additionalProperties": false
      },
      "AIInterview": {
        "type": "object",
        "required": [
          "mode",
          "language",
          "duration_seconds",
          "questions_asked",
          "questions_answered",
          "recording",
          "transcript_url"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "video",
              "audio_only"
            ]
          },
          "language": {
            "type": "string"
          },
          "duration_seconds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "questions_asked": {
            "type": "integer"
          },
          "questions_answered": {
            "type": "integer"
          },
          "recording": {
            "$ref": "#/components/schemas/Recording"
          },
          "transcript_url": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": false
      },
      "Answer": {
        "type": "object",
        "required": [
          "question",
          "answer"
        ],
        "properties": {
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string",
            "description": "Always text. Yes/no answers are \"Yes\" or \"No\"."
          }
        },
        "additionalProperties": false
      },
      "Document": {
        "type": "object",
        "required": [
          "id",
          "label",
          "file_name",
          "content_type",
          "size_bytes",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "What the interview asked for, e.g. \"Your CV\"."
          },
          "file_name": {
            "type": "string"
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "API link (send your key) that returns a download link."
          }
        },
        "additionalProperties": false
      },
      "Candidate": {
        "type": "object",
        "required": [
          "id",
          "attempt",
          "name",
          "email",
          "phone",
          "interview",
          "status",
          "status_label",
          "rejection_reason",
          "started_at",
          "submitted_at",
          "result",
          "ai_interview",
          "answers",
          "documents",
          "dashboard_url"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "attempt": {
            "type": "integer",
            "description": "1 for the first attempt; a retake is a new candidate with attempt 2."
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "interview": {
            "$ref": "#/components/schemas/InterviewRef"
          },
          "status": {
            "type": "string",
            "enum": [
              "in_progress",
              "passed",
              "needs_review",
              "did_not_pass",
              "shortlisted",
              "on_hold",
              "rejected",
              "cancelled",
              "abandoned"
            ]
          },
          "status_label": {
            "type": "string"
          },
          "rejection_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "submitted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the candidate finished every step. Null while they're still taking it."
          },
          "result": {
            "$ref": "#/components/schemas/Result"
          },
          "ai_interview": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AIInterview"
              },
              {
                "type": "null"
              }
            ],
            "description": "Null when the interview has no AI step, or it hasn't started."
          },
          "answers": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Answer"
            },
            "description": "Form answers keyed by the question's id (see the interview's steps.form.questions). The id never changes, even if the question is reworded."
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Document"
            }
          },
          "dashboard_url": {
            "type": "string",
            "format": "uri",
            "description": "The candidate's page in LessRounds (needs a LessRounds sign-in)."
          }
        },
        "additionalProperties": false,
        "description": "One attempt at an interview."
      },
      "FormQuestion": {
        "type": "object",
        "required": [
          "id",
          "text",
          "type",
          "required",
          "choices"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "open_text",
              "mcq",
              "yes_no"
            ]
          },
          "required": {
            "type": "boolean"
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Multiple-choice options; [] for other types."
          }
        },
        "additionalProperties": false
      },
      "FormStep": {
        "type": "object",
        "required": [
          "questions"
        ],
        "properties": {
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FormQuestion"
            }
          }
        },
        "additionalProperties": false
      },
      "AIStep": {
        "type": "object",
        "required": [
          "type",
          "mode",
          "language",
          "time_limit_minutes",
          "question_count",
          "skills"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "scripted",
              "ai_led"
            ],
            "description": "scripted: asks your questions word for word. ai_led: writes its own questions."
          },
          "mode": {
            "type": "string",
            "enum": [
              "video",
              "audio_only"
            ]
          },
          "language": {
            "type": "string"
          },
          "time_limit_minutes": {
            "type": "integer"
          },
          "question_count": {
            "type": "integer"
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The skills candidates are scored on."
          }
        },
        "additionalProperties": false
      },
      "DocumentStep": {
        "type": "object",
        "required": [
          "label",
          "description",
          "accepted_types",
          "required"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "accepted_types": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "required": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "InterviewSteps": {
        "type": "object",
        "required": [
          "form",
          "ai_interview",
          "document"
        ],
        "properties": {
          "form": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FormStep"
              },
              {
                "type": "null"
              }
            ]
          },
          "ai_interview": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AIStep"
              },
              {
                "type": "null"
              }
            ]
          },
          "document": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/DocumentStep"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "additionalProperties": false
      },
      "Interview": {
        "type": "object",
        "required": [
          "id",
          "title",
          "status",
          "language",
          "access_mode",
          "pass_mark",
          "candidate_link",
          "created_at",
          "published_at",
          "closed_at",
          "steps"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "active",
              "paused",
              "closed"
            ]
          },
          "language": {
            "type": "string"
          },
          "access_mode": {
            "type": "string",
            "enum": [
              "public",
              "invite_only"
            ]
          },
          "pass_mark": {
            "type": [
              "integer",
              "null"
            ]
          },
          "candidate_link": {
            "type": "string",
            "format": "uri",
            "description": "The link candidates open."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "closed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "steps": {
            "$ref": "#/components/schemas/InterviewSteps"
          }
        },
        "additionalProperties": false
      },
      "TranscriptEntry": {
        "type": "object",
        "required": [
          "index",
          "question",
          "answer",
          "speaking_seconds"
        ],
        "properties": {
          "index": {
            "type": "integer"
          },
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "speaking_seconds": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "FileDownload": {
        "type": "object",
        "required": [
          "id",
          "name",
          "content_type",
          "size_bytes",
          "download_url",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "content_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Works for 15 minutes, without your key."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "ChangedBy": {
        "type": "object",
        "required": [
          "type",
          "name"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "team",
              "ai",
              "system",
              "candidate"
            ]
          },
          "name": {
            "type": "string",
            "description": "For team: the person's name."
          }
        },
        "additionalProperties": false
      },
      "StatusChange": {
        "type": "object",
        "required": [
          "from",
          "to",
          "by"
        ],
        "properties": {
          "from": {
            "type": "string",
            "description": "Public status code before the change."
          },
          "to": {
            "type": "string",
            "description": "Public status code after the change."
          },
          "by": {
            "$ref": "#/components/schemas/ChangedBy"
          }
        },
        "additionalProperties": false,
        "description": "What a candidate.status_changed event changed, frozen when it happened."
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "type",
          "created_at",
          "api_version",
          "test",
          "change",
          "candidate"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stays the same if you receive the event again — use it to ignore repeats."
          },
          "type": {
            "type": "string",
            "enum": [
              "candidate.submitted",
              "candidate.scored",
              "candidate.ready",
              "candidate.status_changed"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it happened."
          },
          "api_version": {
            "type": "string",
            "const": "v1"
          },
          "test": {
            "type": "boolean",
            "description": "true for samples sent from LessRounds; real events are false."
          },
          "change": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StatusChange"
              },
              {
                "type": "null"
              }
            ],
            "description": "Only on candidate.status_changed."
          },
          "candidate": {
            "$ref": "#/components/schemas/Candidate",
            "description": "The candidate's latest data — not frozen at the time of the event."
          }
        },
        "additionalProperties": false,
        "description": "One entry of the change history. submitted, scored and ready happen once per attempt; status_changed on every change after submission. Nothing is recorded before a candidate submits, or for previews."
      },
      "RecordingDownload": {
        "type": "object",
        "required": [
          "status",
          "content_type",
          "duration_seconds",
          "download_url",
          "expires_at"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "ready",
              "failed",
              "none",
              "deleted"
            ]
          },
          "content_type": {
            "type": "string"
          },
          "duration_seconds": {
            "type": [
              "integer",
              "null"
            ]
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "Works for 15 minutes, without your key."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      }
    }
  }
}
