{
  "openapi": "3.1.0",
  "info": {
    "title": "Woodstitch API \u2014 human ratings for TTS",
    "version": "1",
    "summary": "Paid human listeners judge your text-to-speech audio in blinded studies and you get a number back.",
    "description": "Two tasks only: pairwise preference (tts_preference.v1: a listener hears two clips and picks one, forced choice) and human-vs-AI detection (tts_detect.v1: a listener hears one clip, answers human or AI, rates confidence 1\u20134). Not offered: MOS, CMOS, MUSHRA or any 1\u20135 rating scale.\n\nFlow: POST /start if you have no key (creates the account, returns a key with the first-study credit) \u2192 POST /files (upload) \u2192 POST /evals (free quote; dry_run prices without storing) \u2192 POST /evals/{id}/start (SPENDS MONEY; show the quote to a human first and pass max_total_usd) \u2192 GET /evals/{id} (poll; provisional numbers) \u2192 GET /evals/{id}/results and /export.\n\nAgent rules: never call /start without the user confirming the quoted amount; send Idempotency-Key on every POST and retry a timeout with the same key; pass max_total_usd on every start. Human-readable reference: https://woodstitch.com/docs (markdown at https://woodstitch.com/docs.md). Discovery file: https://woodstitch.com/llms.txt.",
    "termsOfService": "https://woodstitch.com/legal/privacy",
    "contact": {
      "name": "Woodstitch support",
      "email": "support@woodstitch.com",
      "url": "https://woodstitch.com/contact"
    },
    "x-provider": {
      "name": "Woodstitch LLC",
      "jurisdiction": "Colorado, United States",
      "methodology": "https://woodstitch.com/research/methodology"
    }
  },
  "servers": [
    {
      "url": "https://api.woodstitch.com/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "files",
      "description": "Upload audio from disk; ids are usable anywhere create takes a URL."
    },
    {
      "name": "evals",
      "description": "Create (free), start (paid), poll, results, export, cancel."
    },
    {
      "name": "account",
      "description": "Balance, spend and top-up."
    },
    {
      "name": "public",
      "description": "No authentication required."
    }
  ],
  "paths": {
    "/files": {
      "post": {
        "tags": [
          "files"
        ],
        "summary": "Upload audio",
        "operationId": "uploadFiles",
        "description": "Multipart. One file as `file=@x.wav`, or several as repeated `file[]` parts: up to 100 files or 1 GB per call, 50 MB per file. Each file is decoded, measured and stored content-addressed (re-sending the same bytes is harmless). If one file in a call is bad, the call fails and nothing is recorded.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "file[]": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Files in the order sent. A single-file call returns that file object with the same `files` list attached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "description": "`upload_too_large`: a call over 1 GB or a file over 50 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "files"
        ],
        "summary": "Your uploads, newest first",
        "operationId": "listFiles",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/files/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/fileId"
        }
      ],
      "get": {
        "tags": [
          "files"
        ],
        "summary": "One upload",
        "operationId": "getFile",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/File"
                }
              }
            }
          },
          "400": {
            "description": "`bad_file`: not on this account or audio gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "delete": {
        "tags": [
          "files"
        ],
        "summary": "Remove an upload from your account",
        "operationId": "deleteFile",
        "description": "Studies that already used the file keep their copy.",
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/evals": {
      "post": {
        "tags": [
          "evals"
        ],
        "summary": "Create and price an evaluation (charges nothing)",
        "operationId": "createEval",
        "description": "Resolves every clip (a file id or a public http(s) URL it downloads once; redirects not followed), checks it decodes, measures duration, stores an immutable copy and returns a quote. Send `dry_run: true` to price without storing anything. Job identity is a hash of the audio content plus question and rating count, so re-creating the same study returns the existing one with `duplicate: true`. Quotes last 24 hours.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEval"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The evaluation with its quote. Nothing has been charged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "`idempotency_conflict`: key reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "evals"
        ],
        "summary": "Your last 50 evaluations",
        "operationId": "listEvals",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "evals": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EvalProgress"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/evals/{id}/start": {
      "parameters": [
        {
          "$ref": "#/components/parameters/evalId"
        }
      ],
      "post": {
        "tags": [
          "evals"
        ],
        "summary": "Start the evaluation (SPENDS MONEY)",
        "operationId": "startEval",
        "description": "Cannot be undone once raters begin answering. Needs a positive balance; billing is metered from the first judgement, so the whole quote is not reserved. ALWAYS confirm the quoted amount with a human before calling this, and pass `max_total_usd`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "quote_id": {
                    "type": "integer",
                    "description": "If given, must match the current quote, else `quote_mismatch`."
                  },
                  "max_total_usd": {
                    "type": "number",
                    "description": "Hard ceiling. Fails with `over_max` rather than overspending."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalProgress"
                }
              }
            }
          },
          "400": {
            "description": "`over_max`, `quote_expired`, `quote_mismatch`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`insufficient_funds` (balance empty) or `spend_cap` (key cap would be exceeded).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/evals/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/evalId"
        }
      ],
      "get": {
        "tags": [
          "evals"
        ],
        "summary": "Progress and live score",
        "operationId": "getEval",
        "description": "Cheap; poll as often as you like. Returns the submitted design, `judgements.done/target/percent`, `pairs_with_data`, and the same `metrics`/`counts` shape as /results computed from whatever has been rated so far, flagged `provisional: true` until completed. Everything needed to resume is in this response. Treat an interim figure as a preview: stopping because a number crossed a threshold is optional stopping and inflates false positives.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalProgress"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/evals/{id}/results": {
      "parameters": [
        {
          "$ref": "#/components/parameters/evalId"
        }
      ],
      "get": {
        "tags": [
          "evals"
        ],
        "summary": "Final numbers",
        "operationId": "getResults",
        "description": "Available once `completed`, or `canceled` (then `incomplete: true` with `judgements_target`). 409 `not_ready` while running. `metrics` is a flat numeric dict (drops straight into a tracker).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/PreferenceResults"
                    },
                    {
                      "$ref": "#/components/schemas/DetectionResults"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`not_ready`: still running.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/evals/{id}/export": {
      "parameters": [
        {
          "$ref": "#/components/parameters/evalId"
        },
        {
          "name": "format",
          "in": "query",
          "schema": {
            "type": "string",
            "enum": [
              "json",
              "csv"
            ],
            "default": "json"
          }
        }
      ],
      "get": {
        "tags": [
          "evals"
        ],
        "summary": "Per-judgement rows",
        "operationId": "exportEval",
        "description": "One row per judgement with every clustering dimension: rater (pseudonymous, stable within the eval), source, speaker, passage, duration bucket, answer, confidence, time spent. Fit your own mixed-effects model on this.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rows": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Judgement"
                      }
                    }
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/evals/{id}/cancel": {
      "parameters": [
        {
          "$ref": "#/components/parameters/evalId"
        }
      ],
      "post": {
        "tags": [
          "evals"
        ],
        "summary": "Stop early",
        "operationId": "cancelEval",
        "description": "Charges only for work already handed to raters; anyone mid-task finishes and is paid. Check `max_additional_charge_if_canceled_now_usd` on the progress response first if you want that number before deciding.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "responses": {
          "200": {
            "description": "Canceled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvalProgress"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/account": {
      "get": {
        "tags": [
          "account"
        ],
        "summary": "Balance and spend",
        "operationId": "getAccount",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "balance_usd": {
                      "type": "number"
                    },
                    "reserved_usd": {
                      "type": "number"
                    },
                    "spent_usd": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/topup": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Add funds",
        "operationId": "topup",
        "description": "A paused evaluation resumes automatically when the money lands.",
        "responses": {
          "200": {
            "description": "Checkout or confirmation details."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/templates": {
      "get": {
        "tags": [
          "public"
        ],
        "summary": "Both templates: what the listener is asked, inputs, metric",
        "operationId": "getTemplates",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pricing": {
      "get": {
        "tags": [
          "public"
        ],
        "summary": "Rate card",
        "operationId": "getPricing",
        "security": [],
        "description": "USD per hour of audio a listener hears (18 at the time of writing), per judgement, rounded up to the cent; priced from measured duration, never from a number you send.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "public"
        ],
        "summary": "Service root: self-describing summary of the flow",
        "operationId": "root",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/start": {
      "post": {
        "tags": [
          "public"
        ],
        "summary": "Create an account with the first-study credit and get a key (no auth)",
        "operationId": "start",
        "security": [],
        "description": "For an agent asked to run a listening test with no account yet. Creates the account in the background, grants the credit (25 USD at the time of writing), returns an api_key to send as Authorization: Bearer on every later call. Rate-limited per address. Follow https://woodstitch.com/llms.txt for the full first-study recipe.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "string",
                    "description": "Where this is coming from, e.g. the git remote origin. Becomes the account name."
                  },
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Optional. The person's email, only if they gave it; lets them sign in to the dashboard later."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "string"
                    },
                    "credit_usd": {
                      "type": "number"
                    },
                    "account": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "email": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "source": {
                          "type": "string"
                        }
                      }
                    },
                    "keep": {
                      "type": "string"
                    },
                    "spend": {
                      "type": "string"
                    },
                    "dashboard": {
                      "type": "string"
                    },
                    "next": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "`email_in_use`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/account/email": {
      "post": {
        "tags": [
          "account"
        ],
        "summary": "Attach an email to the account (for dashboard sign-in)",
        "operationId": "setAccountEmail",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string"
                    },
                    "dashboard": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "`email_in_use`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer ws_live_...`. Keys are created at https://woodstitch.com/app/api-keys."
      }
    },
    "parameters": {
      "evalId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer"
        }
      },
      "fileId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^file_"
        }
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 128
        },
        "description": "Send on every POST. A retry with the same key returns the original response instead of acting twice; the same key with a different body is `idempotency_conflict`."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`bad_audio`, `fetch_failed`, `bad_file`, `bad_allocation`, `no_files`, `upload_failed`, `too_many_files`, `over_max`, `quote_expired`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`no_key` / `bad_key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such evaluation on this account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "hint": {
                "type": "string"
              }
            }
          }
        }
      },
      "File": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "file"
          },
          "name": {
            "type": "string"
          },
          "duration_s": {
            "type": "number"
          },
          "format": {
            "type": "string"
          },
          "bytes": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FileList": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "list"
          },
          "count": {
            "type": "integer"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/File"
            }
          }
        }
      },
      "AudioRef": {
        "type": "string",
        "description": "A file id from POST /files (`file_\u2026`) or a public http(s) URL. wav, mp3, flac, ogg, m4a; 0.5\u2013120 s; max 50 MB. Use the same format and bitrate on both sides of a pair."
      },
      "Pair": {
        "type": "object",
        "required": [
          "id",
          "a",
          "b"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64,
            "description": "Unique within the evaluation; echoed in per_pair."
          },
          "a": {
            "$ref": "#/components/schemas/AudioRef"
          },
          "b": {
            "$ref": "#/components/schemas/AudioRef"
          },
          "system_a": {
            "type": "string",
            "description": "Label for side a (e.g. model id). Never shown to raters."
          },
          "system_b": {
            "type": "string"
          },
          "passage": {
            "type": "string",
            "description": "Text id shared by every rendering of the same text."
          },
          "speaker": {
            "type": "string",
            "description": "Voice id."
          }
        }
      },
      "Clip": {
        "type": "object",
        "required": [
          "id",
          "audio",
          "truth",
          "speaker",
          "passage"
        ],
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "audio": {
            "$ref": "#/components/schemas/AudioRef"
          },
          "truth": {
            "type": "string",
            "enum": [
              "human",
              "ai"
            ],
            "description": "Never sent to a listener."
          },
          "source": {
            "type": "string",
            "description": "Model id; real narration uses the reserved value `human` (may be omitted)."
          },
          "speaker": {
            "type": "string",
            "description": "Voice id (AI) or narrator id (human). Required: the model-level interval resamples over it."
          },
          "passage": {
            "type": "string",
            "description": "Text id; the same id for every rendering of the same text across sources."
          },
          "duration_bucket": {
            "type": "string",
            "maxLength": 16,
            "description": "Your design condition, e.g. 15s / 30s / 60s."
          }
        }
      },
      "PerParticipant": {
        "type": "object",
        "description": "Rules for one listener. Works on both templates.",
        "properties": {
          "max_judgements": {
            "type": "integer",
            "description": "Cap on how many items one listener ever hears in this study."
          },
          "unique_by": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "speaker",
                "passage",
                "source",
                "truth",
                "duration_bucket",
                "system"
              ]
            },
            "description": "Fields a listener never meets twice; `unique_by: [\"speaker\",\"passage\"]` stops a rater recognising a voice they already judged."
          },
          "max_per": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Cap on how often a listener meets one value of a field, e.g. {\"system\": 7}."
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Fixed mix per sitting (detection)."
          }
        }
      },
      "Allocation": {
        "type": "object",
        "description": "Study-wide balancing (detection only; refused on preference).",
        "properties": {
          "total_judgements": {
            "type": "integer"
          },
          "balance_by": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CreateEval": {
        "type": "object",
        "properties": {
          "template": {
            "type": "string",
            "enum": [
              "tts_preference.v1",
              "tts_detect.v1"
            ],
            "default": "tts_preference.v1"
          },
          "name": {
            "type": "string",
            "default": "eval"
          },
          "question": {
            "type": "string",
            "maxLength": 200,
            "description": "Shown verbatim to raters; per-template default."
          },
          "pairs": {
            "type": "array",
            "maxItems": 5000,
            "items": {
              "$ref": "#/components/schemas/Pair"
            },
            "description": "Preference studies."
          },
          "clips": {
            "type": "array",
            "maxItems": 5000,
            "items": {
              "$ref": "#/components/schemas/Clip"
            },
            "description": "Detection studies."
          },
          "ratings_per_pair": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 5,
            "description": "Independent judgements per pair (`judgments_per_item` accepted as alias)."
          },
          "ratings_per_clip": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50,
            "default": 5
          },
          "per_participant": {
            "$ref": "#/components/schemas/PerParticipant"
          },
          "allocation": {
            "$ref": "#/components/schemas/Allocation"
          },
          "dry_run": {
            "type": "boolean",
            "default": false,
            "description": "Price it and throw it away; nothing stored, no eval_id."
          }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "total_usd": {
            "type": "number"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EvalCreated": {
        "type": "object",
        "properties": {
          "eval_id": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "quote": {
            "$ref": "#/components/schemas/Quote"
          },
          "nothing_charged_yet": {
            "type": "boolean",
            "const": true
          },
          "duplicate": {
            "type": "boolean"
          },
          "pair_seconds": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Every pair's measured listening time and price per judgement."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "effective_design": {
            "type": "object",
            "description": "The compiled plan: judgements_total and how they are allocated."
          },
          "dashboard_url": {
            "type": "string",
            "format": "uri"
          },
          "poll_after_s": {
            "type": "integer"
          }
        }
      },
      "EvalProgress": {
        "type": "object",
        "properties": {
          "eval_id": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "ready",
              "running",
              "completed",
              "canceled",
              "failed"
            ]
          },
          "provisional": {
            "type": "boolean"
          },
          "question": {
            "type": "string"
          },
          "ratings_per_pair": {
            "type": "integer"
          },
          "cost_usd": {
            "type": "number"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "judgements": {
            "type": "object",
            "properties": {
              "done": {
                "type": "integer"
              },
              "target": {
                "type": "integer"
              },
              "percent": {
                "type": "number"
              }
            }
          },
          "pairs_with_data": {
            "type": "integer"
          },
          "max_additional_charge_if_canceled_now_usd": {
            "type": "number"
          },
          "metrics": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "counts": {
            "type": "object"
          },
          "dashboard_url": {
            "type": "string",
            "format": "uri"
          },
          "poll_after_s": {
            "type": "integer"
          }
        }
      },
      "PreferenceResults": {
        "type": "object",
        "properties": {
          "metrics": {
            "type": "object",
            "description": "Flat numeric dict: human_pref/b_win_rate (share of preference for your b side; 0.5 = indistinguishable), human_pref/b_win_rate_ci95_low, human_pref/b_win_rate_ci95_high, human_pref/tie_rate (0 on API studies: forced choice), human_pref/pairs, human_pref/judgements.",
            "additionalProperties": {
              "type": "number"
            }
          },
          "counts": {
            "type": "object",
            "properties": {
              "a_wins": {
                "type": "integer"
              },
              "b_wins": {
                "type": "integer"
              },
              "ties": {
                "type": "integer"
              }
            }
          },
          "per_pair": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "a_wins": {
                  "type": "integer"
                },
                "b_wins": {
                  "type": "integer"
                },
                "ties": {
                  "type": "integer"
                }
              }
            }
          },
          "incomplete": {
            "type": "boolean"
          },
          "judgements_target": {
            "type": "integer"
          }
        }
      },
      "DetectionResults": {
        "type": "object",
        "properties": {
          "metric": {
            "type": "string",
            "const": "human_label_rate"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string"
                },
                "duration": {
                  "type": "string"
                },
                "human_label_rate": {
                  "type": "number",
                  "description": "Share of judgements that called the clip human: fooling rate for AI, recognition rate for `human`."
                },
                "n": {
                  "type": "integer"
                },
                "ci95": {
                  "$ref": "#/components/schemas/TwoScopeCI"
                },
                "baseline_deficit": {
                  "type": "number",
                  "description": "Human rate minus this source's rate; 0 = indistinguishable from real narration on these stimuli."
                },
                "baseline_deficit_ci95": {
                  "$ref": "#/components/schemas/TwoScopeCI"
                }
              }
            }
          },
          "generalisation": {
            "type": "object",
            "description": "Machine-readable statement of what the data supports (e.g. to_the_model: not_established with fewer than 8 voices)."
          },
          "coverage": {
            "type": "object"
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "TwoScopeCI": {
        "type": "object",
        "description": "tested_stimuli resamples raters only (a claim about these files); model resamples raters, voices and passages (a claim about the model) and is typically 3\u20134\u00d7 wider. Quote the model interval for any sentence of the form 'model X fools listeners Y% of the time'.",
        "properties": {
          "tested_stimuli": {
            "$ref": "#/components/schemas/Interval"
          },
          "model": {
            "$ref": "#/components/schemas/Interval"
          }
        }
      },
      "Interval": {
        "type": "object",
        "properties": {
          "low": {
            "type": "number"
          },
          "high": {
            "type": "number"
          }
        }
      },
      "Judgement": {
        "type": "object",
        "properties": {
          "rater": {
            "type": "string",
            "description": "Pseudonymous, stable within the eval."
          },
          "pair_id": {
            "type": "string"
          },
          "clip_id": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "speaker": {
            "type": "string"
          },
          "passage": {
            "type": "string"
          },
          "duration_bucket": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "confidence": {
            "type": "integer"
          },
          "time_spent_ms": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}