{
  "openapi": "3.1.0",
  "info": {
    "title": "Mutanex Report Analytics API",
    "version": "v1",
    "summary": "Clinical report analysis. Send a lab report as text or PDF, get structured data.",
    "description": "Output is **clinical decision support**. It requires review by a qualified\nclinician and must not be used as the sole basis for a clinical decision.\nEvery successful response repeats this in `meta.intended_use`.\n\nReports are processed and discarded. Report text and file contents are never\nretained and never logged, and customer data is never used to train models.",
    "contact": {
      "name": "Mutanex API support",
      "email": "support@mutanex.com"
    }
  },
  "servers": [
    {
      "url": "https://platform.mutanex.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange your credentials for an access token."
    },
    {
      "name": "Reports",
      "description": "Analyse a clinical report."
    },
    {
      "name": "Account",
      "description": "Your own usage and remaining allowance."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "createAccessToken",
        "summary": "Get an access token",
        "description": "OAuth 2.0 client credentials. Authenticate with **HTTP Basic** -\n`client_id` as username, `client_secret` as password, exactly as the cURL\nexample shows. Credentials sent as body fields are refused with a 400\nexplaining the header to use.\n\nThe body carries `grant_type` and `scope`, both required, as\n**`application/x-www-form-urlencoded`** - not JSON. `curl -d` sends this by\ndefault; a JSON body is refused with `invalid_request` naming the encoding.\n\nAsking for a scope you do not have returns `invalid_scope` and **names the\nrefused scopes**.\n\nProtocol errors (400, 401) use the RFC 6749 §5.2 shape; transport and\nlimit errors (413, 429, 500) use the platform envelope.",
        "security": [
          {
            "clientBasic": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              },
              "example": {
                "grant_type": "client_credentials",
                "scope": "reports:text.analyze reports:file.analyze"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A token valid for one hour.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                },
                "example": {
                  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtleS0xIn0…",
                  "token_type": "Bearer",
                  "expires_in": 3600,
                  "scope": "reports:text.analyze reports:file.analyze"
                }
              }
            }
          },
          "400": {
            "description": "Bad grant type, missing `scope`, malformed request, credentials sent as body fields (`invalid_client` - use the Authorization header), a refused scope, or an application/account no longer permitted to issue tokens (`unauthorized_client`).",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "401": {
            "description": "Client authentication failed.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthError"
                }
              }
            }
          },
          "413": {
            "description": "Request body over 4 KB.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many token requests (limit 0.5/second per source address). `details.limit_type` is `source_address` (per-address) or `authentication_capacity` (the process-wide verification backstop).",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying. Honour it rather than guessing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` - an unexpected failure on our side.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/reports/text/analyze": {
      "post": {
        "tags": [
          "Reports"
        ],
        "operationId": "analyzeReportText",
        "summary": "Analyse a report as text",
        "description": "Use this when you already hold the report as text - no OCR step, so it is the faster path, and nothing is written to disk.\n\n**Scope** `reports:text.analyze`\n\n**Body limit** 4 MiB of UTF-8 JSON (`413` beyond it; a non-`utf-*` charset `415`). Non-Latin scripts near the character ceiling can exceed it in UTF-8 - split them.\n\n**Billing** 1,500 characters = 1 page, `max(1, pages / 12)` units. `meta.pages` is omitted - an estimate is not reported as a measurement.\n\n**Latency** seconds to minutes - long reports are split and processed in parts; set your client timeout to at least 10 minutes.\n\n**Limits** 10 requests/second, 5 concurrent per account. A success consumes one free analysis, whatever the size; failures consume nothing. Grants default to 100 - `GET /v1/credits` reports your live balance.\n\n**Do not retry a slow response** - retrying doubles the work and counts twice.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TextAnalyzeRequest"
              },
              "example": {
                "report_text": "HAEMOGLOBIN 13.2 g/dL ...",
                "report_type": "lab_panel"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Analysis complete.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalysisResponse"
                },
                "example": {
                  "request_id": "req_9x8y7z6w5v4u",
                  "data": {
                    "patient_information": {
                      "patient_name": {
                        "value": "A. Sample",
                        "confidence": "0.98"
                      },
                      "age": {
                        "value": "42",
                        "confidence": "0.99"
                      },
                      "gender": {
                        "value": "F",
                        "confidence": "0.99"
                      },
                      "referred_by_doctor": {
                        "value": "",
                        "confidence": ""
                      },
                      "client_name": {
                        "value": "",
                        "confidence": ""
                      },
                      "collection_date": {
                        "value": "2026-07-28",
                        "confidence": "0.97"
                      },
                      "report_release_date": {
                        "value": "2026-07-29",
                        "confidence": "0.97"
                      }
                    },
                    "tests": {
                      "Complete Blood Count": {
                        "test_name": "Complete Blood Count",
                        "sample_type": "EDTA Whole Blood",
                        "specialization": "Generic",
                        "parameters": {
                          "Hemoglobin": {
                            "result": "13.2",
                            "unit": "g/dL",
                            "reference_interval": "12.0-16.0",
                            "confidence": "0.95"
                          }
                        }
                      }
                    },
                    "report_metadata": {
                      "patient_id": {
                        "value": "SMP001",
                        "confidence": "0.96"
                      }
                    },
                    "keyFindings": [
                      "Haemoglobin within reference range."
                    ],
                    "abnormalValues": [],
                    "riskLevel": "low",
                    "riskClassifiedParameters": [],
                    "riskGroupedJson": {
                      "critical": [],
                      "high": [],
                      "low": []
                    }
                  },
                  "meta": {
                    "intended_use": "Clinical decision support. Output requires review by a qualified clinician.",
                    "requires_clinician_review": true,
                    "schema_version": "v1",
                    "engine_version": "2.0.1",
                    "report_type": "lab_panel",
                    "units": 1,
                    "processing_ms": 8400
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` - The request could not be understood.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_token` - The access token is missing, malformed, expired, or no longer valid.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`allowance_exhausted` - The free allowance for this section is exhausted. Contact us to raise it.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_blocked` - This account is blocked.\n\n`endpoint_not_entitled` - This account is not entitled to the requested endpoint.\n\n`insufficient_scope` - The access token does not carry the scope required for this endpoint.\n\n`ip_not_allowed` - Requests are not permitted from this source address.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large` - The request or file exceeds the permitted size.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type` - The file type is not supported.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` - The request failed validation.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` - Too many requests.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying. Honour it rather than guessing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` - An unexpected error occurred.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`engine_error` - The analysis engine failed to process this request.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "`engine_timeout` - The analysis engine did not respond in time.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/reports/file/analyze": {
      "post": {
        "tags": [
          "Reports"
        ],
        "operationId": "analyzeReportFile",
        "summary": "Analyse a report as a PDF",
        "description": "Supply the PDF either as a `file_url` we fetch or as a multipart upload - exactly one; both or neither returns `422 validation_failed`.\n\n**Scope** `reports:file.analyze`\n\n**Type** PDF only (`application/pdf`) - determined by file contents, never by name or declared Content-Type.\n\n**Size** upload 20 MiB; `file_url` fetch 50 MiB - that path bypasses the JSON body ceiling entirely - completing within 20 seconds including up to 3 redirects.\n\n**Billing** measured page count: `max(1, pages / 12)` units.\n\n**Latency** seconds to minutes - long reports are split and processed in parts; set your client timeout to at least 10 minutes.\n\n**Limits** 10 requests/second, 5 concurrent per account. A success consumes one free analysis, whatever the size; failures consume nothing. Grants default to 100 - `GET /v1/credits` reports your live balance.\n\n**Fetch failures** `file_unreachable` is transient (slow, blocked, or 5xx host) - a retry may help; `file_not_found` (host 4xx, e.g. an expired signed URL) and a malformed `file_url` (`422`) - retries never fix.\n\n**Do not retry a slow response** - retrying doubles the work and counts twice.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FileAnalyzeJsonRequest"
              },
              "example": {
                "file_url": "https://example.com/report.pdf",
                "report_type": "lab_panel"
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "report_type"
                ],
                "oneOf": [
                  {
                    "required": [
                      "file"
                    ],
                    "not": {
                      "required": [
                        "file_url"
                      ]
                    }
                  },
                  {
                    "required": [
                      "file_url"
                    ],
                    "not": {
                      "required": [
                        "file"
                      ]
                    }
                  }
                ],
                "x-exactly-one": [
                  "file",
                  "file_url"
                ],
                "additionalProperties": false,
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The PDF, maximum 20 MiB. Supply either this part or a `file_url` text field - exactly one. Type is determined by file contents, never by name or declared Content-Type.",
                    "x-accepted-media-types": [
                      "application/pdf"
                    ]
                  },
                  "file_url": {
                    "type": "string",
                    "maxLength": 2048,
                    "format": "uri",
                    "description": "HTTPS URL we fetch the PDF from. Must be publicly resolvable on port 443 and require no credentials - use a signed URL. Mutually exclusive with a multipart file upload."
                  },
                  "report_type": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "A label of your choosing for what kind of report this is, e.g. \"lab_panel\". No allowlist - any label your workflow already uses is accepted. Echoed back as meta.report_type so a stored result carries your own label. It is a label for the KIND of report - never put patient identifiers in it. The engine does not read it, and it is not stored on your usage events.",
                    "examples": [
                      "lab_panel"
                    ]
                  },
                  "language": {
                    "description": "BCP 47 language tag, e.g. \"en-CA\". Accepted and validated, but it currently has no effect: this engine release takes no language parameter, so the tag is not used, stored, or echoed back.",
                    "examples": [
                      "en-CA"
                    ],
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 35
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Analysis complete.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalysisResponse"
                },
                "example": {
                  "request_id": "req_9x8y7z6w5v4u",
                  "data": {
                    "patient_information": {
                      "patient_name": {
                        "value": "A. Sample",
                        "confidence": "0.98"
                      },
                      "age": {
                        "value": "42",
                        "confidence": "0.99"
                      },
                      "gender": {
                        "value": "F",
                        "confidence": "0.99"
                      },
                      "referred_by_doctor": {
                        "value": "",
                        "confidence": ""
                      },
                      "client_name": {
                        "value": "",
                        "confidence": ""
                      },
                      "collection_date": {
                        "value": "2026-07-28",
                        "confidence": "0.97"
                      },
                      "report_release_date": {
                        "value": "2026-07-29",
                        "confidence": "0.97"
                      }
                    },
                    "tests": {
                      "Complete Blood Count": {
                        "test_name": "Complete Blood Count",
                        "sample_type": "EDTA Whole Blood",
                        "specialization": "Generic",
                        "parameters": {
                          "Hemoglobin": {
                            "result": "13.2",
                            "unit": "g/dL",
                            "reference_interval": "12.0-16.0",
                            "confidence": "0.95"
                          }
                        }
                      }
                    },
                    "report_metadata": {
                      "patient_id": {
                        "value": "SMP001",
                        "confidence": "0.96"
                      }
                    },
                    "keyFindings": [
                      "Haemoglobin within reference range."
                    ],
                    "abnormalValues": [],
                    "riskLevel": "low",
                    "riskClassifiedParameters": [],
                    "riskGroupedJson": {
                      "critical": [],
                      "high": [],
                      "low": []
                    }
                  },
                  "meta": {
                    "intended_use": "Clinical decision support. Output requires review by a qualified clinician.",
                    "requires_clinician_review": true,
                    "schema_version": "v1",
                    "engine_version": "2.0.1",
                    "report_type": "lab_panel",
                    "pages": 3,
                    "units": 1,
                    "processing_ms": 8400
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` - The request could not be understood.\n\n`file_unreachable` - The supplied file_url could not be retrieved.\n\n`file_not_found` - The supplied file_url did not return a file.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_token` - The access token is missing, malformed, expired, or no longer valid.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`allowance_exhausted` - The free allowance for this section is exhausted. Contact us to raise it.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_blocked` - This account is blocked.\n\n`endpoint_not_entitled` - This account is not entitled to the requested endpoint.\n\n`insufficient_scope` - The access token does not carry the scope required for this endpoint.\n\n`ip_not_allowed` - Requests are not permitted from this source address.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large` - The request or file exceeds the permitted size.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type` - The file type is not supported.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`validation_failed` - The request failed validation.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` - Too many requests.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying. Honour it rather than guessing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` - An unexpected error occurred.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`engine_error` - The analysis engine failed to process this request.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "`engine_timeout` - The analysis engine did not respond in time.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getCredits",
        "summary": "Remaining free allowance",
        "description": "Your own account only, resolved from the token. There is no account identifier to pass and no way to read another account.\n\nDuring the pilot this reports your **free allowance**. Purchased credits will\nappear in this same response when billing launches - the URL never changes.",
        "responses": {
          "200": {
            "description": "Current allowance.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreditsResponse"
                },
                "example": {
                  "request_id": "req_a1b2c3d4e5f6",
                  "account_id": "acc_7k2m9p4q1w8e",
                  "free_allowance": [
                    {
                      "section": "1",
                      "label": "Report analysis",
                      "granted": 100,
                      "remaining": 87
                    }
                  ],
                  "as_of": "2026-08-01T12:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token (`invalid_token`).",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_blocked` - this account is blocked.\n\n`ip_not_allowed` - requests are not permitted from this source address.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many account reads (limit 10/second per account, shared by `/v1/credits` and `/v1/usage`). `details.limit_type` is `requests_per_second`.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying. Honour it rather than guessing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` - an unexpected failure on our side.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Account"
        ],
        "operationId": "getUsage",
        "summary": "Usage summary",
        "description": "Per-endpoint calls, units and latency percentiles. Defaults to the last 30\ndays. Your own account only.\n\nAggregates are computed over at most 5,000 events, newest first -\n`truncated` tells you when that cap was hit.",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "examples": [
                "2026-08-01T00:00:00Z"
              ]
            },
            "description": "RFC 3339 date-time, e.g. `2026-08-01T00:00:00Z`. Offsets like `-04:00` are accepted and normalised to UTC. Defaults to 30 days before `to`. Inclusive; the window is half-open (`from <= created_at < to`), so reusing one call's `to` as the next call's `from` tiles windows with no gap and no double counting."
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "examples": [
                "2026-09-01T00:00:00Z"
              ]
            },
            "description": "Same format as `from`. Exclusive (see `from`). Defaults to now."
          },
          {
            "name": "endpoint_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "examples": [
                "reports.text.analyze",
                "reports.file.analyze"
              ]
            },
            "description": "Restrict to one endpoint id: `reports.text.analyze` or `reports.file.analyze`. An unknown id returns 422 naming the valid values."
          }
        ],
        "responses": {
          "200": {
            "description": "Usage in the requested window.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                },
                "example": {
                  "request_id": "req_f6e5d4c3b2a1",
                  "account_id": "acc_7k2m9p4q1w8e",
                  "from": "2026-07-01T00:00:00.000Z",
                  "to": "2026-08-01T00:00:00.000Z",
                  "truncated": false,
                  "by_endpoint": [
                    {
                      "endpoint_id": "reports.text.analyze",
                      "calls": 12,
                      "successes": 11,
                      "failures": 1,
                      "units": 13.5,
                      "free_consumed": 11,
                      "latency_p50_ms": 900,
                      "latency_p95_ms": 2100
                    },
                    {
                      "endpoint_id": "reports.file.analyze",
                      "calls": 7,
                      "successes": 7,
                      "failures": 0,
                      "units": 9.0833,
                      "free_consumed": 7,
                      "latency_p50_ms": 3200,
                      "latency_p95_ms": 5400
                    }
                  ],
                  "as_of": "2026-08-01T12:00:00Z"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token (`invalid_token`).",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`account_blocked` - this account is blocked.\n\n`ip_not_allowed` - requests are not permitted from this source address.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`from` is not earlier than `to`, a timestamp is malformed, or `endpoint_id` is unknown.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many account reads (limit 10/second per account, shared by `/v1/credits` and `/v1/usage`). `details.limit_type` is `requests_per_second`.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait before retrying. Honour it rather than guessing."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`internal_error` - an unexpected failure on our side.",
            "headers": {
              "X-Request-Id": {
                "schema": {
                  "type": "string"
                },
                "description": "The request id, on every response. Send your own (8-64 characters of letters, digits, `-` or `_`) and it is adopted and echoed back."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "The `access_token` from `POST /oauth/token`, sent as\n`Authorization: Bearer <token>`.\n\nTokens last 3600 seconds - fetch one and **reuse it until it expires**, not one per call.\nA long lifetime does not mean slow revocation: account status, endpoint access\nand credential validity are re-checked on every single request, so a rotated\nsecret or a suspended account takes effect on the very next call."
      },
      "clientBasic": {
        "type": "http",
        "scheme": "basic",
        "description": "For `POST /oauth/token` only: `client_id` as username, `client_secret` as password, each application/x-www-form-urlencoded-encoded per RFC 6749 §2.3.1 (plain values are fine for the issued `cid_...`/`mnx_sk_...` formats). The only client authentication - credentials never go in the body."
      }
    },
    "schemas": {
      "TextAnalyzeRequest": {
        "type": "object",
        "properties": {
          "report_text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500000,
            "description": "The report as plain text, 1 to 500,000 characters. Faster than the file endpoint - no OCR step - and nothing is written to disk."
          },
          "report_type": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A label of your choosing for what kind of report this is, e.g. \"lab_panel\". No allowlist - any label your workflow already uses is accepted. Echoed back as meta.report_type so a stored result carries your own label. It is a label for the KIND of report - never put patient identifiers in it. The engine does not read it, and it is not stored on your usage events.",
            "examples": [
              "lab_panel"
            ]
          },
          "language": {
            "description": "BCP 47 language tag, e.g. \"en-CA\". Accepted and validated, but it currently has no effect: this engine release takes no language parameter, so the tag is not used, stored, or echoed back.",
            "examples": [
              "en-CA"
            ],
            "type": "string",
            "minLength": 2,
            "maxLength": 35
          }
        },
        "required": [
          "report_text",
          "report_type"
        ],
        "additionalProperties": false
      },
      "FileAnalyzeRequest": {
        "type": "object",
        "properties": {
          "file_url": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "HTTPS URL we fetch the PDF from. Must be publicly resolvable on port 443 and require no credentials - use a signed URL. Mutually exclusive with a multipart file upload."
          },
          "report_type": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A label of your choosing for what kind of report this is, e.g. \"lab_panel\". No allowlist - any label your workflow already uses is accepted. Echoed back as meta.report_type so a stored result carries your own label. It is a label for the KIND of report - never put patient identifiers in it. The engine does not read it, and it is not stored on your usage events.",
            "examples": [
              "lab_panel"
            ]
          },
          "language": {
            "description": "BCP 47 language tag, e.g. \"en-CA\". Accepted and validated, but it currently has no effect: this engine release takes no language parameter, so the tag is not used, stored, or echoed back.",
            "examples": [
              "en-CA"
            ],
            "type": "string",
            "minLength": 2,
            "maxLength": 35
          }
        },
        "required": [
          "report_type"
        ],
        "additionalProperties": false
      },
      "FileAnalyzeJsonRequest": {
        "type": "object",
        "properties": {
          "file_url": {
            "type": "string",
            "maxLength": 2048,
            "format": "uri",
            "description": "HTTPS URL we fetch the PDF from - required here, because a JSON body has no way to carry the file itself. Must be publicly resolvable on port 443 and require no credentials: use a signed URL."
          },
          "report_type": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A label of your choosing for what kind of report this is, e.g. \"lab_panel\". No allowlist - any label your workflow already uses is accepted. Echoed back as meta.report_type so a stored result carries your own label. It is a label for the KIND of report - never put patient identifiers in it. The engine does not read it, and it is not stored on your usage events.",
            "examples": [
              "lab_panel"
            ]
          },
          "language": {
            "description": "BCP 47 language tag, e.g. \"en-CA\". Accepted and validated, but it currently has no effect: this engine release takes no language parameter, so the tag is not used, stored, or echoed back.",
            "examples": [
              "en-CA"
            ],
            "type": "string",
            "minLength": 2,
            "maxLength": 35
          }
        },
        "required": [
          "report_type",
          "file_url"
        ],
        "additionalProperties": false
      },
      "TokenRequest": {
        "type": "object",
        "required": [
          "grant_type",
          "scope"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "client_credentials",
            "description": "Tokens are issued to an application's own credentials, never on behalf of an end user - there is no authorization-code or password flow."
          },
          "scope": {
            "type": "string",
            "description": "Required. Space-delimited. Available: reports:text.analyze, reports:file.analyze"
          }
        }
      },
      "TokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "description": "Send as `Authorization: Bearer <token>` on every API call. Treat it as opaque - the internal format may change within `v1`."
          },
          "token_type": {
            "type": "string",
            "const": "Bearer",
            "description": "Always `Bearer` - the token travels in the `Authorization` header, never in a query string."
          },
          "expires_in": {
            "type": "integer",
            "examples": [
              3600
            ],
            "description": "Seconds until expiry, counted from issue. Request a new token after it elapses - there are no refresh tokens."
          },
          "scope": {
            "type": "string",
            "description": "The scopes granted, space-delimited - identical to what you requested, because an unknown or ungranted scope is refused with `invalid_scope` rather than silently dropped."
          }
        }
      },
      "AnalysisResponse": {
        "type": "object",
        "required": [
          "request_id",
          "data",
          "meta"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Your request id - also the `X-Request-Id` response header."
          },
          "data": {
            "$ref": "#/components/schemas/ReportData"
          },
          "meta": {
            "$ref": "#/components/schemas/AnalysisMeta"
          }
        }
      },
      "ReportData": {
        "type": "object",
        "required": [
          "patient_information",
          "tests",
          "report_metadata",
          "keyFindings",
          "abnormalValues",
          "riskLevel"
        ],
        "description": "The structured report. Documented fields are stable within `v1` and new fields may be **added**; ignore fields you do not recognise rather than failing on them.",
        "properties": {
          "patient_information": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/ValueWithConfidence"
            },
            "description": "Patient and referral facts read from the report header: `patient_name`, `age`, `gender`, `referred_by_doctor`, `client_name`, `collection_date`, `report_release_date`. Values the report did not show are empty strings, never invented."
          },
          "tests": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/LabTest"
            },
            "description": "Keyed by test name (e.g. `\"Complete Blood Count\"`). **Only tests found in the report appear** - an empty object means no recognised test."
          },
          "report_metadata": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/ValueWithConfidence"
            },
            "description": "Laboratory bookkeeping read from the report - sample ids, timestamps, signatories - **and repeated patient identifiers** (`patient_id`, `patient_name_repeat`), so PHI handling must cover this object too. Same value/confidence shape as `patient_information`."
          },
          "keyFindings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Clinician-facing highlights, concise, PHI-aware."
          },
          "abnormalValues": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Out-of-range parameters, named."
          },
          "riskLevel": {
            "type": "string",
            "description": "Overall report risk as assessed by the insights pass."
          },
          "riskClassifiedParameters": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RiskClassifiedParameter"
            },
            "description": "**Absent - not empty - when risk classification degraded**; `meta.warnings` says so. Absence must not be rendered as \"nothing was risky\"."
          },
          "riskGroupedJson": {
            "type": "object",
            "description": "Abnormal parameters grouped by severity tier. Absent alongside `riskClassifiedParameters` when classification degraded.",
            "properties": {
              "critical": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "high": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "low": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              }
            }
          }
        }
      },
      "ValueWithConfidence": {
        "type": "object",
        "required": [
          "value",
          "confidence"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "As read from the report; empty when not shown."
          },
          "confidence": {
            "type": "string",
            "description": "Extraction confidence, `\"0\"`-`\"1\"` as a string; empty when not applicable."
          }
        }
      },
      "LabTest": {
        "type": "object",
        "required": [
          "test_name"
        ],
        "properties": {
          "test_name": {
            "type": "string",
            "description": "The test's name as recognised from the report - the same string as its key in `tests`."
          },
          "sample_type": {
            "type": "string",
            "description": "Specimen the test was run on, as stated by the report (e.g. `\"EDTA Whole Blood\"`). Empty when the report does not say."
          },
          "specialization": {
            "type": "string",
            "description": "Laboratory discipline the test belongs to (e.g. `\"HEMATOLOGY\"`). Empty when the report does not say."
          },
          "parameters": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/LabParameter"
            },
            "description": "Keyed by parameter name (e.g. `\"Hemoglobin\"`), on the tests that are parameter-based. Only parameters with a result appear. **Absent on culture, histopathology and similar tests**, whose findings arrive under their own keys (`organism_isolated`, `gross_examination`, ...) rather than as a parameter map."
          }
        },
        "additionalProperties": true
      },
      "LabParameter": {
        "type": "object",
        "properties": {
          "result": {
            "type": "string",
            "description": "Present on parameters that report a value."
          },
          "unit": {
            "type": "string",
            "description": "Present on measured parameters."
          },
          "reference_interval": {
            "type": "string",
            "description": "Present on measured parameters."
          },
          "confidence": {
            "type": "string",
            "description": "Present on measured parameters."
          }
        },
        "additionalProperties": true
      },
      "RiskClassifiedParameter": {
        "type": "object",
        "required": [
          "name",
          "result",
          "unit",
          "reference_interval",
          "riskLevel"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Parameter name, matching its key under the owning test's `parameters` map."
          },
          "result": {
            "type": "string",
            "description": "The measured value exactly as read from the report - never recomputed, converted or rounded."
          },
          "unit": {
            "type": "string",
            "description": "Unit as printed on the report."
          },
          "reference_interval": {
            "type": "string",
            "description": "The normal range printed on the report itself - the issuing lab's interval for its own method and population, not a universal range."
          },
          "riskLevel": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high",
              "critical",
              "unknown"
            ],
            "description": "Severity of this parameter's deviation, classified by the insights pass. `unknown` means it could not be judged - surface it for clinician review, never render it as normal."
          },
          "note": {
            "type": "string",
            "description": "Clinical note, present only for high/critical parameters."
          }
        }
      },
      "AnalysisMeta": {
        "type": "object",
        "description": "Response metadata: the clinical-safety statement, the schema and engine versions, the report type, the billable units, and any degradation warnings.",
        "required": [
          "intended_use",
          "requires_clinician_review",
          "schema_version",
          "engine_version",
          "report_type",
          "units",
          "processing_ms"
        ],
        "properties": {
          "intended_use": {
            "type": "string",
            "description": "The clinical-safety statement, in-band on every success so the result testifies to what it is - decision support, not a diagnosis - wherever it is stored, logged, or forwarded beyond this documentation."
          },
          "requires_clinician_review": {
            "type": "boolean",
            "const": true,
            "description": "Machine-actionable safety gate: key display or filing logic on this (e.g. show a review banner, block auto-filing) rather than parsing `intended_use` prose."
          },
          "schema_version": {
            "type": "string",
            "const": "v1",
            "description": "The response-contract version this payload conforms to. A breaking change ships as a new version, announced - never in place."
          },
          "engine_version": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always present; null only if the engine version cannot be resolved."
          },
          "report_type": {
            "type": "string",
            "description": "The `report_type` you sent, echoed back unchanged. This echo is the whole of what the field does: it lets a stored or forwarded result carry your own label for what was analysed. The engine does not read it."
          },
          "pages": {
            "type": "integer",
            "description": "**File endpoint only.** Pages counted in your PDF. Absent on the text endpoint, where no document exists to measure."
          },
          "warnings": {
            "type": "array",
            "description": "Present only when the analysis degraded: `data` is usable but incomplete. For example, `RISK_CLASSIFICATION_UNAVAILABLE` means risk grouping failed - treat that section as *unavailable*, never as \"nothing risky found\". Branch on `code`; `message` wording may change.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "description": "Stable machine identifier for the degradation. Branch on this."
                },
                "message": {
                  "type": "string",
                  "description": "Human explanation, for logs and tickets. Wording may change - never branch on it."
                }
              }
            }
          },
          "units": {
            "type": "number",
            "description": "Billable units for this call. See each endpoint for how it is derived."
          },
          "processing_ms": {
            "type": "integer",
            "description": "Engine time, excluding transport."
          }
        }
      },
      "CreditsResponse": {
        "type": "object",
        "required": [
          "request_id",
          "account_id",
          "free_allowance",
          "as_of"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Your request id - also the `X-Request-Id` response header."
          },
          "account_id": {
            "type": "string",
            "description": "Your account id (`acc_...`)."
          },
          "free_allowance": {
            "type": "array",
            "description": "One entry per price-book section your account draws from.",
            "items": {
              "type": "object",
              "required": [
                "section",
                "label",
                "granted",
                "remaining"
              ],
              "properties": {
                "section": {
                  "type": "string",
                  "description": "Price-book section number as a string - the stable key. Both `/v1/reports/...` endpoints draw from section `\"1\"`."
                },
                "label": {
                  "type": "string",
                  "description": "Human name for the section, e.g. `Report analysis`. Display this; branch on `section`."
                },
                "granted": {
                  "type": "integer",
                  "description": "Free analyses granted in total."
                },
                "remaining": {
                  "type": "integer",
                  "description": "Free analyses left."
                }
              }
            }
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server time the balance was read. A concurrent call can consume allowance the moment after, so treat it as a snapshot, not a reservation."
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "required": [
          "request_id",
          "account_id",
          "from",
          "to",
          "truncated",
          "by_endpoint",
          "as_of"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Your request id - also the `X-Request-Id` response header."
          },
          "account_id": {
            "type": "string",
            "description": "Your account id (`acc_...`)."
          },
          "from": {
            "type": "string",
            "format": "date-time",
            "description": "Start of the aggregation window, inclusive (UTC). Defaults to 30 days before `to`."
          },
          "to": {
            "type": "string",
            "format": "date-time",
            "description": "End of the window, **exclusive**. Reuse this value as the next request's `from` to tile windows with no gap and no double count."
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the 5,000-event cap was hit: the aggregates cover only the newest 5,000 calls in the window, and the echoed `from` is the requested bound, not where coverage starts. Narrow the range with `from`/`to`."
          },
          "by_endpoint": {
            "type": "array",
            "description": "One aggregate row per endpoint that recorded at least one call in the window - an endpoint with no traffic has no row.",
            "items": {
              "type": "object",
              "required": [
                "endpoint_id",
                "calls",
                "successes",
                "failures",
                "units",
                "free_consumed",
                "latency_p50_ms",
                "latency_p95_ms"
              ],
              "properties": {
                "endpoint_id": {
                  "type": "string",
                  "description": "The endpoint's stable id (e.g. `reports.text.analyze`) - the same id used in entitlements and error `details.endpoint_id`."
                },
                "calls": {
                  "type": "integer",
                  "description": "Recorded calls in the window: successful analyses plus accepted-then-failed ones."
                },
                "successes": {
                  "type": "integer",
                  "description": "Calls that returned `200` - each consumed one free analysis."
                },
                "failures": {
                  "type": "integer",
                  "description": "Analysis calls that were accepted and then failed - request validation, file fetch, or the engine. Refusals that fire before the endpoint runs (authentication, entitlement, oversized bodies) are not recorded. Failed calls do not consume your free allowance."
                },
                "units": {
                  "type": "number",
                  "description": "Billable units summed over successful calls (see \"How usage is counted\"). Failed calls contribute zero."
                },
                "free_consumed": {
                  "type": "integer",
                  "description": "Free analyses consumed in the window - one per success, so it tracks `successes` and never exceeds it."
                },
                "latency_p50_ms": {
                  "type": "integer",
                  "description": "Over successful calls in the window."
                },
                "latency_p95_ms": {
                  "type": "integer",
                  "description": "Over successful calls in the window."
                }
              }
            }
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "Server time the aggregates were computed."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "request_id"
        ],
        "description": "The platform error envelope. Every endpoint uses it - `/oauth/token` included for transport and limit errors (413, 429, 500); only that endpoint's OAuth protocol errors (400, 401) use the RFC 6749 shape instead. **Branch on `error.code`, not on `message`** - codes and their HTTP statuses are fixed for the life of `v1`; messages may be reworded.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "file_unreachable",
                  "file_not_found",
                  "invalid_token",
                  "account_blocked",
                  "endpoint_not_entitled",
                  "insufficient_scope",
                  "ip_not_allowed",
                  "allowance_exhausted",
                  "payload_too_large",
                  "unsupported_media_type",
                  "validation_failed",
                  "rate_limited",
                  "engine_error",
                  "engine_timeout",
                  "not_found",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "object",
                "additionalProperties": true
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        }
      },
      "OAuthError": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "RFC 6749 §5.2. Used by `/oauth/token` for OAuth protocol errors (400, 401). Its 413, 429 and 500 responses use the standard `Error` envelope instead.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "invalid_client",
              "unauthorized_client",
              "unsupported_grant_type",
              "invalid_scope"
            ]
          },
          "error_description": {
            "type": "string"
          }
        }
      }
    }
  }
}