openapi: 3.1.0
info:
  title: SequenxAI Time Series Inference API
  version: 1.0.0
  description: |
    Official OpenAPI 3.1 contract for SequenxAI time series foundation services.

    **Data format:** Requests use long-table fields (`unique_id`, `ds`, `y`) plus forecast
    controls (`h`, `freq`, `level`). Responses carry an auditable `data_quality` report
    describing the cleaning applied to the input. Enterprise private deployment
    (Docker / Helm) is contracted separately.

    **Live models:** The public model is SequenxAI Omni, model id `hora-1.0` (the floating alias `hora`
    resolves to it; responses always echo `hora-1.0`). It is used when `model` is omitted; any
    other id returns 422 `MODEL_NOT_AVAILABLE`. The SequenxAI control plane serves live inference at
    `https://api.sequenxai.com/v1`: it authenticates tenant `sqx_live_*` keys (issued by
    operators or on the SequenxAI Platform), runs the cleaning stage, then the model. Billing counts only
    the observed points fed to the model plus the forecast points; built-in cleaning and
    `/clean` are not billed. The `…/cn/v1` server URL remains a contract reservation until regional
    routing is edge-mounted. There is no sandbox host: try models in the platform's forecast
    workbench (`https://platform.sequenxai.com/forecast`) or call `/v1` with a free-tier key from the
    SequenxAI Platform (`https://platform.sequenxai.com`).

    **Availability:** Only `/forecast` and the beta `/clean` are served today. Operations
    marked `x-availability: planned` (`/anomaly-detection`, `/batch/forecast`,
    `/batch/jobs/{job_id}`) are designed shapes kept for review; calls currently return 404.

    **Regions & privatization:** Public cloud endpoints keep series data in the selected
    region. Mainland China production traffic should prefer the `cn-mainland` server once
    edge-routed; until then use `/v1`. Air-gapped / VPC private deployments are contracted
    separately; the wire format matches this OpenAPI document so SDKs do not fork.
  contact:
    name: SequenxAI Developer Support
    email: contact@sequenxai.com
    url: https://sequenxai.com
  x-sequenx-deployment:
    public_regions:
      - id: cn-mainland
        data_residency: PRC
        notes: Intended PRC residency region once …/cn/v1 is edge-routed; until then mainland live traffic uses /v1 (manual keys).
      - id: global
        data_residency: configurable
        notes: Non-PRC and international traffic.
    privatization:
      modes: [docker, helm]
      contract_parity: Same OpenAPI paths and schemas as public cloud; auth and quota may differ per license.

tags:
  - name: Inference
    description: Online real-time time series forecasting operations
  - name: Anomaly Detection
    description: Not yet available (planned). Statistical and deep learning anomaly detection
  - name: Batch Processing
    description: Not yet available (planned). High-throughput asynchronous batch operations

servers:
  - url: https://api.sequenxai.com/v1
    description: Live control plane (tenant sqx_live_* keys). Mainland traffic currently uses this path.
  - url: https://api.sequenxai.com/cn/v1
    description: Contract-reserved cn-mainland path — not edge-routed yet; do not assume this prefix works today.

security:
  - BearerAuth: []

paths:
  /forecast:
    post:
      tags:
        - Inference
      summary: Real-time Online Time Series Forecast
      description: |
        Performs online inference for one or multiple time series entities using SequenxAI
        foundation models. Accepts flat long-table records. Multi-model routing is out of
        scope for this contract revision.
      operationId: createForecast
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - $ref: "#/components/parameters/IdempotencyKeyHeader"
        - $ref: "#/components/parameters/RegionHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ForecastRequest"
            examples:
              power_meter_example:
                summary: Single entity with weather covariates
                value:
                  model: hora-1.0
                  series:
                    - unique_id: "meter-001"
                      ds: "2026-09-01T00:00:00Z"
                      y: 142.5
                      temperature: 28.4
                      humidity: 65.0
                    - unique_id: "meter-001"
                      ds: "2026-09-01T01:00:00Z"
                      y: 138.2
                      temperature: 27.9
                      humidity: 68.0
                  future_covariates:
                    - unique_id: "meter-001"
                      ds: "2026-09-02T00:00:00Z"
                      temperature: 29.1
                      is_holiday: 0
                    - unique_id: "meter-001"
                      ds: "2026-09-02T01:00:00Z"
                      temperature: 28.5
                      is_holiday: 0
                  h: 24
                  freq: "1h"
                  level: [80, 90]
                  clean_series: true
                  clean_exog: true
              point_quantile_example:
                summary: P70 point forecast with 80 and 90 intervals
                value:
                  model: hora-1.0
                  series:
                    - unique_id: "meter-001"
                      ds: "2026-09-01T00:00:00Z"
                      y: 142.5
                    - unique_id: "meter-001"
                      ds: "2026-09-01T01:00:00Z"
                      y: 138.2
                  h: 24
                  freq: "1h"
                  level: [80, 90]
                  point_quantile: 0.7
              model_options_example:
                summary: Model options (always clamp at 0, single pass)
                value:
                  model: hora-1.0
                  series:
                    - unique_id: "meter-001"
                      ds: "2026-09-01T00:00:00Z"
                      y: 142.5
                    - unique_id: "meter-001"
                      ds: "2026-09-01T01:00:00Z"
                      y: 138.2
                  h: 24
                  freq: "1h"
                  level: [80, 90]
                  model_options:
                    nonnegative: true
                    flip_average: false
      responses:
        "200":
          description: Successful forecast response with point prediction, intervals, and data quality report
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Client-Request-Id:
              $ref: "#/components/headers/ClientRequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit-Type:
              $ref: "#/components/headers/RateLimitLimitType"
            X-RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
            X-Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ForecastResponse"
              examples:
                power_meter_success:
                  summary: Forecast with cleaning report
                  value:
                    request_id: "req_01J8K9XQW5ABCD1234"
                    model_used: "hora-1.0"
                    freq: "1h"
                    forecast:
                      - unique_id: "meter-001"
                        ds: "2026-09-02T00:00:00Z"
                        y_hat: 150.3
                        intervals:
                          "80":
                            lo: 142.1
                            hi: 158.5
                          "90":
                            lo: 138.4
                            hi: 162.0
                    data_quality:
                      status: cleaned
                      inferred_freq: "1h"
                      missing_ratio: 0.04
                      imputed_points: 1
                      outlier_points_suppressed: 0
                      notes:
                        - "1 missing y filled via local continuity interpolation"
                    usage:
                      series_count: 1
                      input_data_points: 24
                      forecast_data_points: 24
                      latency_ms: 118
                    effective_policy:
                      id: "default@2026-13"
                      preset: default
                      version: "2026-13"
                      source: platform
                    model_input:
                      - unique_id: "meter-001"
                        context_points: 25
                        fed_points: 24
                        truncated: 0
                        missing_mode: filled
                        covariates_used: false
                    quantile_report:
                      policy: nearest
                      requested: [0.05, 0.1, 0.5, 0.9, 0.95]
                      used: [0.05, 0.1, 0.5, 0.9, 0.95]
                      substitutions: []
                substituted_levels:
                  summary: Level 95 served by the nearest native quantiles 0.01 and 0.99
                  value:
                    request_id: "req_01J8K9XQW5ABCD5678"
                    model_used: "hora-1.0"
                    freq: "1h"
                    forecast:
                      - unique_id: "meter-001"
                        ds: "2026-09-02T00:00:00Z"
                        y_hat: 150.3
                        intervals:
                          "98":
                            lo: 131.7
                            hi: 169.4
                    data_quality:
                      status: ok
                      missing_ratio: 0.0
                    usage:
                      series_count: 1
                      input_data_points: 24
                      forecast_data_points: 24
                      latency_ms: 97
                    quantile_report:
                      policy: nearest
                      requested: [0.025, 0.5, 0.975]
                      used: [0.01, 0.5, 0.99]
                      substitutions:
                        - requested: 0.025
                          used: 0.01
                        - requested: 0.975
                          used: 0.99
                point_quantile_success:
                  summary: P70 as y_hat (point_quantile 0.7, native on hora-1.0)
                  value:
                    request_id: "req_01J8K9XQW5ABCD9012"
                    model_used: "hora-1.0"
                    freq: "1h"
                    point_quantile: 0.7
                    forecast:
                      - unique_id: "meter-001"
                        ds: "2026-09-02T00:00:00Z"
                        y_hat: 154.6
                        intervals:
                          "80":
                            lo: 142.1
                            hi: 158.5
                          "90":
                            lo: 138.4
                            hi: 162.0
                    data_quality:
                      status: ok
                      missing_ratio: 0.0
                    usage:
                      series_count: 1
                      input_data_points: 24
                      forecast_data_points: 24
                      latency_ms: 104
                    quantile_report:
                      policy: nearest
                      requested: [0.05, 0.1, 0.5, 0.7, 0.9, 0.95]
                      used: [0.05, 0.1, 0.5, 0.7, 0.9, 0.95]
                      substitutions: []
                      point_forecast:
                        requested: 0.7
                        used: 0.7
                        method: native
        "400":
          description: |
            Invalid payload syntax or unparseable JSON (`MALFORMED_JSON`), or an `Idempotency-Key`
            that is not 1–255 printable ASCII characters or disagrees with the deprecated
            `X-Idempotency-Key` alias (`VALIDATION_ERROR`, `details.field` names the header).
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                malformed_json:
                  $ref: "#/components/examples/malformed_json"
        "401":
          description: Authentication failed or API Key missing, deleted or revoked
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalid_api_key:
                  $ref: "#/components/examples/invalid_api_key"
        "403":
          description: |
            Authenticated caller lacks permission (scope or IP allowlist), the key's account or
            workspace is suspended by SequenxAI (`ACCOUNT_SUSPENDED`, beta), or the workspace is in a deletion
            cool-off (`ORG_PENDING_DELETION`, beta). A suspended account's or workspace's keys stay valid and
            work again once it is restored. Not billed.
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                insufficient_scope:
                  $ref: "#/components/examples/insufficient_scope"
                ip_not_allowed:
                  $ref: "#/components/examples/ip_not_allowed"
                account_suspended:
                  $ref: "#/components/examples/account_suspended"
                org_pending_deletion:
                  $ref: "#/components/examples/org_pending_deletion"
        "409":
          description: |
            `IDEMPOTENCY_IN_PROGRESS`: a request with the same `Idempotency-Key` is still running;
            retry after it finishes to receive the stored result. Replays with an identical body
            return the original result without double-charging usage.
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                idempotency_pending:
                  $ref: "#/components/examples/idempotency_pending"
        "422":
          description: |
            Business validation or cleaning failure (e.g. missing ds column, future table contains y,
            discontinuity beyond repair threshold, frequency inference failure). A `model` other
            than `hora-1.0` / `hora` is `MODEL_NOT_AVAILABLE`; a `model_options` key the model does
            not take is `MODEL_OPTION_UNSUPPORTED`; both are rejected before any quota is reserved.
            On routes that accept
            `Idempotency-Key`, also `IDEMPOTENCY_KEY_REUSED`: the same key was sent with a different
            request body within the 24-hour window (nothing ran or was billed).
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                model_not_available:
                  $ref: "#/components/examples/model_not_available"
                model_option_unsupported:
                  $ref: "#/components/examples/model_option_unsupported"
                idempotency_body_mismatch:
                  $ref: "#/components/examples/idempotency_body_mismatch"
                future_covariates_invalid:
                  $ref: "#/components/examples/future_covariates_invalid"
                series_discontinuous:
                  $ref: "#/components/examples/series_discontinuous"
                freq_inference_failed:
                  $ref: "#/components/examples/freq_inference_failed"
                timestamp_irregular:
                  $ref: "#/components/examples/timestamp_irregular"
                ambiguous_date_format:
                  $ref: "#/components/examples/ambiguous_date_format"
                points_exceeded:
                  $ref: "#/components/examples/points_exceeded"
                freq_unsupported:
                  $ref: "#/components/examples/freq_unsupported"
                freq_ambiguous:
                  $ref: "#/components/examples/freq_ambiguous"
                series_too_short:
                  $ref: "#/components/examples/series_too_short"
                horizon_exceeds_model:
                  $ref: "#/components/examples/horizon_exceeds_model"
                quantile_unsupported:
                  $ref: "#/components/examples/quantile_unsupported"
                point_quantile_unsupported:
                  $ref: "#/components/examples/point_quantile_unsupported"
                series_empty:
                  $ref: "#/components/examples/series_empty"
                duplicate_timestamps:
                  $ref: "#/components/examples/duplicate_timestamps"
                cleaning_option_unsupported:
                  $ref: "#/components/examples/cleaning_option_unsupported"
                cleaning_option_requires_version:
                  $ref: "#/components/examples/cleaning_option_requires_version"
                validation_error:
                  $ref: "#/components/examples/validation_error"
                cleaning_option_unknown:
                  $ref: "#/components/examples/cleaning_option_unknown"
                raw_input_invalid:
                  $ref: "#/components/examples/raw_input_invalid"
                covariate_column_mismatch:
                  $ref: "#/components/examples/covariate_column_mismatch"
                covariate_limit_exceeded:
                  $ref: "#/components/examples/covariate_limit_exceeded"
        "429":
          description: Rate limit exceeded or quota exhausted
          headers:
            Deprecation:
              $ref: "#/components/headers/IdempotencyKeyAliasDeprecation"
            Retry-After:
              description: Seconds to wait before retrying
              schema:
                type: integer
                example: 5
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit-Type:
              $ref: "#/components/headers/RateLimitLimitType"
            X-RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                rate_limit_exceeded:
                  $ref: "#/components/examples/rate_limit_exceeded"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/ServiceUnavailableError"
        "504":
          $ref: "#/components/responses/GatewayTimeoutError"

  /clean:
    post:
      tags:
        - Inference
      summary: Cleaning Dry Run (free, rate-limited)
      description: |
        Beta. Runs the same cleaning stage as `/forecast` and returns only the cleaning report
        (with every automatic decision) and the effective policy: no inference, no forecast.
        Served by the control plane. Requires a tenant API Key, is not billed and does not
        consume the daily point quota (each call is recorded with zero billable points), but
        has its own limits: 60 requests per minute per account, shared by all of the account's
        keys (429 `RATE_LIMIT_EXCEEDED`, `limit_type: rpm`) and at most 200,000 input points per request (422
        `POINTS_EXCEEDED`). Cleaning capacity is shared by all accounts and dry runs never queue:
        while it is full the call returns 503 `SERVICE_UNAVAILABLE` with `Retry-After`. With a
        `model`, a series longer than that model's maximum context returns 422 `SERIES_TOO_LONG`,
        exactly as `/forecast` would. The `raw`
        preset skips cleaning, so it has no report to show and returns 422
        `CLEANING_OPTION_UNSUPPORTED`; structural problems of raw input are reported by
        `/forecast` (`RAW_INPUT_INVALID`).
      operationId: cleanSeries
      x-beta: true
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CleanRequest"
            examples:
              default_preset:
                summary: Dry run with the default preset
                value:
                  series:
                    - unique_id: "meter-001"
                      ds: "2026-09-01T00:00:00Z"
                      y: 142.5
                    - unique_id: "meter-001"
                      ds: "2026-09-01T01:00:00Z"
                      y: null
                    - unique_id: "meter-001"
                      ds: "2026-09-01T02:00:00Z"
                      y: 138.2
                  freq: "1h"
              enhanced_policy:
                summary: Dry run under policy version 2026-13 with a declared range
                value:
                  series:
                    - unique_id: "pv-01"
                      ds: "2026-06-01T00:00:00Z"
                      y: 0.0
                    - unique_id: "pv-01"
                      ds: "2026-06-01T01:00:00Z"
                      y: null
                    - unique_id: "pv-01"
                      ds: "2026-06-01T02:00:00Z"
                      y: 0.0
                  freq: "1h"
                  options:
                    cleaning:
                      preset: default
                      version: "2026-13"
                      bounds:
                        lower: 0
                        upper: 500
      responses:
        "200":
          description: Cleaning report and effective policy
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            X-Client-Request-Id:
              $ref: "#/components/headers/ClientRequestId"
            X-RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            X-RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            X-RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            X-RateLimit-Limit-Type:
              $ref: "#/components/headers/RateLimitLimitType"
            X-RateLimit-Policy:
              $ref: "#/components/headers/RateLimitPolicy"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CleanResponse"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitError"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "503":
          $ref: "#/components/responses/ServiceUnavailableError"

  /anomaly-detection:
    post:
      tags:
        - Anomaly Detection
      summary: Historical Time Series Anomaly Detection
      description: |
        Not yet available: this endpoint is planned; calls currently return 404. The shape
        below is the designed contract, kept for review.

        Evaluates historical observations, computes expected values and dynamic confidence
        bands, and detects contextual and spike anomalies.
      operationId: detectAnomalies
      x-availability: planned
      x-badges:
        - name: Not yet available
          position: before
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - $ref: "#/components/parameters/RegionHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnomalyDetectionRequest"
      responses:
        "200":
          description: Successful anomaly detection response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnomalyDetectionResponse"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /batch/forecast:
    post:
      tags:
        - Batch Processing
      summary: Submit Asynchronous Batch Forecast Job
      description: |
        Not yet available: this endpoint is planned; calls currently return 404. The shape
        below is the designed contract, kept for review.

        Submits an asynchronous forecasting job for high-throughput workloads (thousands to millions of points).
        Accepts pre-signed OSS/S3 URLs for CSV or Parquet files.
      operationId: submitBatchForecast
      x-availability: planned
      x-badges:
        - name: Not yet available
          position: before
      parameters:
        - $ref: "#/components/parameters/RequestIdHeader"
        - $ref: "#/components/parameters/IdempotencyKeyHeader"
        - $ref: "#/components/parameters/RegionHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchForecastRequest"
      responses:
        "202":
          description: Batch job successfully submitted and queued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJobSummary"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "409":
          $ref: "#/components/responses/IdempotencyInProgressError"
        "422":
          $ref: "#/components/responses/ValidationError"
        "429":
          $ref: "#/components/responses/RateLimitError"
        "500":
          $ref: "#/components/responses/InternalServerError"

  /batch/jobs/{job_id}:
    get:
      tags:
        - Batch Processing
      summary: Get Batch Job Status and Download URL
      description: |
        Not yet available: this endpoint is planned; calls currently return 404. The shape
        below is the designed contract, kept for review.

        Retrieves the current execution status, progress, and result download URLs for a batch task.
        Requires a valid Bearer API Key; `job_id` is not a capability URL and results are tenant-scoped.
      operationId: getBatchJob
      x-availability: planned
      x-badges:
        - name: Not yet available
          position: before
      parameters:
        - name: job_id
          in: path
          required: true
          description: Unique batch job identifier
          schema:
            type: string
            example: "job_01J8K9XQW5ABCD1234"
        - $ref: "#/components/parameters/RequestIdHeader"
        - $ref: "#/components/parameters/RegionHeader"
      responses:
        "200":
          description: Detailed batch job status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchJobDetail"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "403":
          $ref: "#/components/responses/ForbiddenError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "429":
          $ref: "#/components/responses/RateLimitError"
        "500":
          $ref: "#/components/responses/InternalServerError"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: APIKey
      description: |
        Pass your SequenxAI API Key as a Bearer token (e.g., Bearer sqx_live_...). Keys are
        tenant keys issued by operators or on the SequenxAI Platform; only a digest is stored, so a
        lost key is deleted on the platform and replaced, never recovered.

  parameters:
    RequestIdHeader:
      name: X-Request-Id
      in: header
      required: false
      description: |
        Your own trace ID. Recorded as the client request id and echoed in the
        `X-Client-Request-Id` response header; it is never used as a key. The server always
        generates its own `X-Request-Id` (`req_…`), which appears in the response body, the
        response header, and support conversations. Values outside the pattern are ignored.
      schema:
        type: string
        pattern: "^[A-Za-z0-9_-]{8,64}$"
        example: "c79439f0-281b-4f9a-9e7f-7128a38a7c21"

    IdempotencyKeyHeader:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Optional idempotency token, scoped to (API Key, route) and kept for 24 hours.
        The first request's final outcome (a 2xx or a deterministic 4xx such as a validation
        error) is stored with a digest of the canonical JSON body; a retry with the same key
        and an identical body returns that stored response, with its original `request_id`
        and `X-Idempotent-Replayed: true`, without running or billing again. The same key with
        a different body returns 422 `IDEMPOTENCY_KEY_REUSED`; a duplicate while the first
        request is still running returns 409 `IDEMPOTENCY_IN_PROGRESS`. Upstream 5xx, timeouts,
        unavailability and 401/403/429 are not stored, so the key can be retried. A value that
        is not 1–255 printable ASCII characters returns 400 `VALIDATION_ERROR`. Implemented on
        `/forecast`; the planned batch route only declares it.

        Deprecated alias: `X-Idempotency-Key` is still accepted for one release. When only the
        alias is sent it is used as the key and the response carries
        `Deprecation: @1791417600` (RFC 9745: deprecated since 2026-10-08); when
        both headers are sent with different values the request fails with 400
        `VALIDATION_ERROR` (`details.field: Idempotency-Key`).
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: "^[\\x20-\\x7E]{1,255}$"
        example: "idemp_order_20260917_001"

    RegionHeader:
      name: X-SequenX-Region
      in: header
      required: false
      description: |
        Preferred data region hint. Transitional live edge currently serves `/v1`
        only — `cn-mainland` residency pinning via a distinct host path is not
        edge-enforced until `…/cn/v1` is mounted. Private deployments may ignore
        or pin this header.
      schema:
        type: string
        enum: [cn-mainland, global]
        example: cn-mainland

  schemas:
    # -------------------------------------------------------------
    # Core Data Point & Series Schemas
    # -------------------------------------------------------------
    TimeSeriesPoint:
      type: object
      required:
        - unique_id
        - ds
        - y
      properties:
        unique_id:
          type: string
          description: Unique identifier for the series entity (e.g. sensor ID, SKU code)
          example: "meter-001"
        ds:
          type: string
          description: |
            Timestamp, ISO 8601 recommended. An explicit offset (`Z`, `+08:00`) always wins.
            Values without an offset are wall-clock times in `options.timezone`, or UTC when
            that option is omitted. Unix seconds / milliseconds are absolute instants.
          example: "2026-09-01T00:00:00Z"
        y:
          type:
            - number
            - "null"
          description: Target observation metric. Null represents missing data points.
          example: 142.5
      additionalProperties:
        type:
          - number
          - string
          - boolean
          - "null"
        description: Historical exogenous covariates flattened as sibling columns (e.g. temperature, humidity).

    FutureCovariatePoint:
      type: object
      required:
        - unique_id
        - ds
      properties:
        unique_id:
          type: string
          description: Entity identifier corresponding to historical series
          example: "meter-001"
        ds:
          type: string
          description: Future timestamp in ISO 8601 format matching the forecast horizon
          example: "2026-09-02T00:00:00Z"
      additionalProperties:
        type:
          - number
          - string
          - boolean
          - "null"
        description: |
          Future known covariates (e.g. weather forecast, promotion schedules).
          Target column 'y' is strictly prohibited in future covariates.

    PredictionInterval:
      type: object
      required:
        - lo
        - hi
      properties:
        lo:
          type: number
          description: Lower bound for the confidence interval
          example: 142.1
        hi:
          type: number
          description: Upper bound for the confidence interval
          example: 158.5

    ForecastResultPoint:
      type: object
      required:
        - unique_id
        - ds
        - y_hat
      properties:
        unique_id:
          type: string
          example: "meter-001"
        ds:
          type: string
          example: "2026-09-02T00:00:00Z"
        y_hat:
          type: number
          description: |
            Point prediction: the model's median (quantile 0.5), not the expected value. With
            `point_quantile` it is that quantile's path instead (the native quantile actually
            served, see `quantile_report.point_forecast`), which may lie outside `intervals`
            (e.g. `point_quantile` 0.95 with `level` 80).
          example: 150.3
        intervals:
          type: object
          description: |
            Map of interval levels (e.g. '80', '90') to their lower and upper bounds. The key is
            the coverage of the quantiles actually used (`(hi quantile - lo quantile) * 100`),
            which differs from the requested `level` when a quantile was substituted (see
            `quantile_report`); requested levels that resolve to the same quantiles appear once.
          additionalProperties:
            $ref: "#/components/schemas/PredictionInterval"
          example:
            "80":
              lo: 142.1
              hi: 158.5
            "95":
              lo: 136.0
              hi: 164.2

    # -------------------------------------------------------------
    # Request & Response Schemas
    # -------------------------------------------------------------
    ForecastRequest:
      type: object
      required:
        - series
        - h
      properties:
        model:
          type: string
          default: "hora-1.0"
          description: |
            Model id to run: `hora-1.0` (SequenxAI Omni), or its floating alias `hora`; the response's
            `model_used` always names the concrete id `hora-1.0`. Any other id is rejected with
            422 `MODEL_NOT_AVAILABLE` (`details.model`, `details.available`) before any quota is
            reserved. Limits of `hora-1.0`: minimum context 16 points, maximum context 4,096 points,
            maximum horizon 1,024 steps.
          example: "hora-1.0"
        model_options:
          $ref: "#/components/schemas/ModelOptions"
        series:
          type: array
          description: |
            Historical flat long-table records. Per request, `series` rows plus `future_covariates`
            rows may not exceed the per-request cap (default 200,000, sized to the cleaning host's
            memory; 422 `POINTS_EXCEEDED` with `details.points` and `details.cap`). Per entity, the
            points sent to the model may not exceed the selected model's maximum context (422
            `SERIES_TOO_LONG`): the API never truncates history, so send only the most recent points.
          items:
            $ref: "#/components/schemas/TimeSeriesPoint"
        future_covariates:
          type: array
          description: |
            Optional future known planning variables. Each column must also appear in the history
            (`series` sibling columns), otherwise 422 `COVARIATE_COLUMN_MISMATCH`; `y` is 422
            `FUTURE_COVARIATES_INVALID`. At most 8 covariate columns in total (422
            `VALIDATION_ERROR`). A column that does not cover every forecast stamp, is not numeric
            or has long gaps is not used, and `model_input[].covariates` says why; the target
            forecast is still returned. Whether a model uses covariates at all depends on the
            model and is reported per column, never silently: `hora-1.0` does not use covariates
            today, so a column that passes these checks is reported with `reason: model_unsupported`.
          items:
            $ref: "#/components/schemas/FutureCovariatePoint"
        h:
          type: integer
          minimum: 1
          maximum: 2048
          description: |
            Forecast horizon (number of future steps to predict). Each model also has its own
            maximum horizon; a larger `h` is rejected with 422 `HORIZON_EXCEEDS_MODEL`
            (`details.max_horizon` names the limit) before any inference runs.
          example: 24
        freq:
          type: string
          description: |
            Case-sensitive, pandas-compatible frequency token with an optional integer multiple.
            Fixed steps: `s`, `min` (minutes; `T` and lowercase `m` are accepted as minutes, e.g.
            '15min', '15m'), `h`, `D`, `W` (`S`, `H`, `d` and `w` are also accepted).
            Calendar grids: `MS` / `ME` (month start / end), `QS` / `QE` (quarter), `YS` / `YE`
            (year); uppercase `M` means month end (`ME`), `Q` means `QE`, `Y` / `A` mean `YE`.
            Lowercase `m` is always minutes, never months.
            If omitted, the server infers the frequency. Responses and the cleaning report echo the
            canonical token (`min`, `h`, `d`, `w`, `MS`, `QE`, ...). Monthly data anchored to a
            constant day other than the 1st or the month end is reported with the gateway
            extension `M@<day>` (e.g. `M@15`, `3M@15`), which is also accepted as input.
            Business days: `B` (Monday to Friday; weekends are not gaps and forecast stamps skip
            them) is accepted and echoed only under cleaning policy versions `2026-13` (the
            platform default) and the deprecated `2026-12` and `2026-11`
            (`options.cleaning.version`), which also detect it for daily data with no weekend dates. The deprecated `2026-10` and the `raw`
            preset reject `B`.
          example: "1h"
        level:
          type: array
          description: |
            List of percentage prediction interval levels to compute (typically 80, 90, 95). Level
            `L` asks for the quantiles `0.5 - L/200` and `0.5 + L/200`. The model only produces
            its native quantiles (`hora-1.0`: 0.01, 0.05, 0.1 to 0.9 in steps of 0.05, 0.95,
            0.99); see `quantile_policy` for levels that need other quantiles.
          items:
            type: integer
            minimum: 1
            maximum: 99
          example: [80, 90]
        quantile_policy:
          type: string
          x-beta: true
          enum: [nearest, strict]
          default: nearest
          description: |
            `nearest`: a quantile the model does not produce natively is served by its nearest
            native quantile (ties go to the one farther from the median), the interval is keyed
            by the level actually delivered and `quantile_report` lists every substitution.
            `strict`: any such quantile is rejected with 422 `QUANTILE_UNSUPPORTED`
            (`details.unsupported`, `details.supported`) before any quota is reserved or
            anything is billed. Applies to `point_quantile` as well.
        point_quantile:
          type: number
          x-beta: true
          minimum: 0.01
          maximum: 0.99
          description: |
            Quantile returned as `y_hat` instead of the median, e.g. 0.7 for a P70 forecast.
            0.01 to 0.99 with at most 3 decimal places; anything else (including booleans and
            strings) is 422 `VALIDATION_ERROR` with `details.errors[].field` `point_quantile`.
            Served from the model's native quantiles under the same `quantile_policy` as `level`:
            `nearest` uses the nearest native quantile (ties go to the one farther from the
            median, e.g. 0.75 on deciles is served by 0.8), `strict` rejects a non-native value
            with 422 `QUANTILE_UNSUPPORTED` (`details.point_quantile`). Never interpolated.
            Independent of `level`; the response echoes the quantile served as `point_quantile`
            and reports it in `quantile_report.point_forecast`. Omitted: `y_hat` is the median
            and the response is unchanged. Does not change billing.
          example: 0.7
        clean_series:
          type: boolean
          default: true
          description: |
            When true, run industrial cleaning on historical `series` (timestamp regularization,
            local continuity imputation, outlier flagging, frequency inference) before inference.
            Reject with SERIES_DISCONTINUOUS / FREQ_INFERENCE_FAILED when repair is unsafe.
            After a gap longer than the split threshold (see `options.gap_split_min_steps`) only the
            latest contiguous segment is kept and listed in `cleaning_report`, never silent. History
            is never cut to fit the model: an entity longer than the model's maximum context after
            cleaning (regular-grid fill included) is rejected with 422 `SERIES_TOO_LONG`.
        clean_exog:
          type: boolean
          default: true
          description: |
            Align covariates to the cleaned grid and fill short gaps (up to `gap_fill_max_steps`)
            linearly. With `false`, covariates are still aligned but any gap makes the column
            unused. Outliers in covariates are never modified.
        options:
          type: object
          properties:
            router_strategy:
              type: string
              enum: [auto, ensemble, single, fast]
              default: auto
              description: |
                Routing policy across base models (P1 capability). Contract reserves the field;
                early deployments may map all values to a single backend without accuracy claims.
            timezone:
              type: string
              x-beta: true
              default: UTC
              description: |
                IANA zone name (e.g. `Asia/Shanghai`) used to read `ds` values that carry no offset.
                Explicit offsets in the data win. Output `ds` stays UTC (`Z`).
              example: "Asia/Shanghai"
            ds_format:
              type: string
              x-beta: true
              enum: [dmy, mdy]
              description: Day/month order for `D/M/YYYY`-style timestamps the data cannot prove on its own.
            gap_split_min_steps:
              type: integer
              x-beta: true
              minimum: 0
              description: |
                Gap length (grid steps) after which only the latest contiguous segment is kept.
                Omitted: automatic threshold, the largest of `gap_fill_max_steps + 1`, three seasonal
                cycles implied by the frequency, and 5% of the series span. `0` disables the split
                (long gaps are then interpolated and warned).
            cleaning:
              type: object
              x-beta: true
              additionalProperties: false
              description: |
                Closed object: a field not listed below is rejected with 422 `VALIDATION_ERROR`
                (`details.errors[].field`, e.g. `options.cleaning.imputer`) before any cleaning runs,
                never ignored.
                Cleaning policy overrides. `accept` applies proposed decisions and `decline` reverts
                applied ones, both by the stable `id` echoed in `cleaning_report.decisions[]`. Ids that
                match nothing change no data and are listed in a `decision_not_applicable` warning.
                `preset` picks the rule set and `version` its rule-set version; each is resolved in
                the order request > API Key setting > organization default > platform default
                (`default@2026-13`) and echoed in `effective_policy`. The knobs below
                (`impute_method` to `long_gap`) require version `2026-11` or later; under the
                deprecated `2026-10` they are rejected with 422 `CLEANING_OPTION_UNSUPPORTED`
                (`details.requires_version`), never ignored. Invalid knob values are 422 `VALIDATION_ERROR`. `raw` accepts none of
                them.
              properties:
                preset:
                  type: string
                  enum: [default, strict, raw]
                  description: |
                    `default` applies the standard cleaning. `strict` only applies lossless
                    normalization (parsing, time zone, ordering, exact duplicates); any decision
                    that would fill, change or drop values is only proposed, and remaining gaps or
                    sentinels are rejected with 422 and the report. `raw` skips cleaning: `ds` must
                    already be strict ISO-8601 with `Z` or an explicit offset, strictly increasing
                    at a constant fixed step equal to `freq` (required; calendar frequencies such as
                    `MS` are not supported), and every `y` finite; otherwise 422
                    `RAW_INPUT_INVALID`. Data is never repaired on the raw path. `raw` cannot be
                    combined with `accept` / `decline`.
                  example: default
                version:
                  type: string
                  enum: ["2026-10", "2026-11", "2026-12", "2026-13"]
                  x-deprecated-values: ["2026-10", "2026-11", "2026-12"]
                  description: |
                    Rule-set version. Omitted: the key or organization setting, else `2026-13`
                    (the platform default and the newest version). Rules and thresholds never
                    change within a version. We recommend omitting this field so requests follow
                    the platform default.
                    **Deprecated:** `2026-10`, `2026-11` and `2026-12` are still accepted and
                    applied unchanged, but will be removed after notice; a request, API Key or
                    organization setting that pins one gets a `cleaning_version_deprecated`
                    warning in `warnings[]`. The removal date will be announced in advance.
                    `2026-10` is the original rule set (every gap filled linearly).
                    `2026-11` adds per-gap imputation (PCHIP for short gaps,
                    seasonal fills for longer ones when a seasonal period is accepted),
                    physical constraints on imputed values (non-negativity, declared bounds,
                    structural zeros such as PV output at night), business-day grids (`freq` `B`)
                    and extra data checks (number formats, constant / all-zero series, every
                    unparseable timestamp located). Observed values are never modified.
                    `2026-12` is `2026-11` plus a fixed forecast origin: the forecast always
                    starts one step after the last `ds` you send, even when its `y` is null
                    (for example a new interval whose value has not arrived yet). Up to
                    min(`gap_fill_max_steps`, 3) trailing null steps are passed to the model as
                    missing, not filled (decision `gap.masked_tail`, issue `masked_tail`); a longer
                    trailing run, or declining that decision, is 422 `SERIES_DISCONTINUOUS`
                    (blocking `trailing_gap_too_long`) instead of a forecast that starts earlier.
                    Under `2026-10` and `2026-11` trailing nulls are dropped (`edge_null`) and the
                    forecast starts after the last non-null `y`.
                    `2026-13` is `2026-12` with a missing-data check based on where values are
                    missing instead of how many: earlier versions refuse any series with 10% or
                    more missing; `2026-13` accepts scattered missing values up to 35% and
                    refuses a long gap just before the forecast start instead. Measured in
                    seasonal periods (24 steps for hourly data, 7 for daily, 52 for weekly),
                    a gap of at least 0.75 periods followed by fewer than 0.5 periods of values,
                    or a missing share of 35% or more, is 422 `SERIES_DISCONTINUOUS` (blocking
                    `health_critical` with `reason` `gap_near_origin_too_long` or
                    `missing_share_too_high`). Shorter recent gaps (from 0.25 periods), other
                    gaps of 2 periods or more and shares from 20% are accepted with the
                    cleaning warning `missing_pattern`. Compared with `2026-12`, some series that
                    were refused for their missing share are now accepted (with that warning when
                    the share is 20% or more), and some series with a long gap just before the
                    forecast start that were accepted are now refused with
                    `gap_near_origin_too_long`.
                  example: "2026-13"
                impute_method:
                  type: string
                  enum: [auto, linear, pchip, seasonal]
                  description: |
                    `2026-11` (default `auto`). `auto` picks per gap: PCHIP for gaps up to the
                    short-gap limit, seasonal for longer gaps when a seasonal period is accepted,
                    linear otherwise. A forced method falls back when it cannot run; every gap in
                    the report states its method, tier and reason.
                seasonal_period:
                  type: integer
                  minimum: 2
                  maximum: 10000
                  description: |
                    `2026-11`. Seasonal period in grid steps. Omitted: inferred from the frequency
                    (daily cycle for sub-daily data, weekly for daily, 5 for business days) and
                    accepted only with enough cycles and autocorrelation.
                nonnegative:
                  type: string
                  enum: ["auto", "true", "false"]
                  description: |
                    `2026-11` for `true` / `false` (`2026-10` accepts only `auto`, which has no
                    effect there). `auto` clips imputed values at 0 when the observed series is
                    non-negative; `true` always clips them and flags observed negatives; `false`
                    turns the check off.
                bounds:
                  type: object
                  additionalProperties: false
                  description: |
                    `2026-11`. Declared physical range: imputed values are clipped to it, observed
                    values outside it are flagged (`out_of_bounds`). At least one bound;
                    `lower <= upper`.
                  properties:
                    lower:
                      type: [number, "null"]
                    upper:
                      type: [number, "null"]
                structural_zero:
                  type: string
                  enum: [auto, "off"]
                  description: |
                    `2026-11` (default `auto`). A seasonal phase that is zero in at least 90% of at
                    least seven cycles stays zero in imputed values.
                business_days:
                  type: string
                  enum: [auto, "on", "off"]
                  description: |
                    `2026-11` (default `auto`). `auto` puts daily data with no weekend dates on a
                    business-day grid (`freq` `B`); `on` forces it for daily data (weekend rows are
                    dropped off-grid); `off` keeps calendar days. Decline the `grid.business_days`
                    decision to undo an automatic grid.
                long_gap:
                  type: string
                  enum: [fill, mask]
                  description: |
                    `2026-11` (default `fill`). How the model reads interior gaps longer than the
                    short-gap limit (`gap_fill_max_steps`, the gaps in the `long_gap` issue).
                    `fill`: the model reads the filled values, as in the cleaning report. `mask`:
                    the cleaning report and the filled values stay the same, but a model that
                    supports missing-value masking receives those positions as missing instead;
                    a model without masking reads the filled values, and the response says so
                    (`model_input[].long_gap_handling: fill`, warning `long_gap_mask_unavailable`)
                    instead of failing. Short gaps are filled for every model either way. Consider
                    `mask` when long gaps are outages or unknown periods whose filled values should
                    not guide the forecast, mainly for long gaps well before the most recent data;
                    keep `fill` when the filled values are a reasonable stand-in (for example a
                    stable seasonal pattern) or a long gap ends close to the last observation. Billing is the same: masked
                    positions were never observed, so they are not billed input points.
                trailing_gap:
                  type: string
                  enum: [limit, extend]
                  description: |
                    `2026-12` and later (default `limit`). By default at most 3 trailing empty
                    values are accepted (see `version`); a longer trailing run is 422
                    `SERIES_DISCONTINUOUS` with blocking `trailing_gap_too_long`, which then also
                    reports `extend_max_steps`, the most `extend` would accept for that entity.
                    `extend` confirms that you want a forecast anyway: up to 24 trailing empty
                    steps are accepted, and never more than `h` or than one seasonal period when
                    the cleaning report accepted one (`series[].seasonality`). Beyond that the
                    request is still 422 (`trailing_gap_too_long` with `max_steps` set to that
                    limit and `bound` naming the limit that applied: `max_steps`, `horizon` or
                    `seasonal_period`). The forecast still starts one step after the last input
                    timestamp. Forecasts become less reliable the more trailing values are
                    missing, especially relative to `h`; every confirmed entity gets a
                    `trailing_values_missing` warning with `confirmed: true` and a qualitative
                    `impact`, and the `gap.masked_tail` decision records the confirmation. When
                    you can, send the missing recent values instead.
                accept:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                    maxLength: 200
                  example: ["value.dash_token:series_1"]
                decline:
                  type: array
                  maxItems: 200
                  items:
                    type: string
                    maxLength: 200
                  example: ["value.sentinel:series_1:-9999"]

    ModelOptions:
      type: object
      additionalProperties: false
      description: |
        Optional inference switches for the model; omitted keys use the defaults below. Closed
        object: an unknown key is 422 `VALIDATION_ERROR`. A key the selected model does not take
        is 422 `MODEL_OPTION_UNSUPPORTED` (`details.model`, `details.unsupported`,
        `details.supported`) before any quota is reserved. `hora-1.0` takes both keys. Billing is
        per forecast point whatever the options.
      properties:
        nonnegative:
          oneOf:
            - type: string
              enum: [auto]
            - type: boolean
          default: auto
          description: |
            Clamping of the forecast at 0. `auto`: every forecast path of an entity (`y_hat` and
            interval bounds) is clamped at 0 only when all of that entity's history fed to the
            model is non-negative; `true`: always clamp at 0; `false`: never clamp. Unrelated to
            `options.cleaning.nonnegative`, which only constrains imputed history values.
          example: auto
        flip_average:
          type: boolean
          default: true
          description: |
            `true`: the model also forecasts the negated history and averages that (mirrored back)
            with the direct forecast, which takes about twice the inference compute and may add
            latency; `false`: a single pass. Billing does not change: it stays per forecast point.
          example: true

    CleaningDecision:
      type: object
      x-beta: true
      additionalProperties: true
      required:
        - id
        - rule
        - class
        - status
      description: |
        One cleaning decision. `applied` changed the data; `proposed` left it untouched until the id is
        passed in `options.cleaning.accept`; `declined` was reverted by `options.cleaning.decline`.
      properties:
        id:
          type: string
          description: Stable across runs of the same data (rule, series and selector; never row numbers).
          example: "value.sentinel:series_1:-9999"
        rule:
          type: string
          example: "value.sentinel"
        class:
          type: string
          enum: [structural, drop, fabricate, semantic]
          description: |
            `structural` moves or aligns points, `drop` removes observations, `fabricate` inserts values
            that were not observed, `semantic` reinterprets observed values.
        status:
          type: string
          enum: [applied, proposed, declined]
        unique_id:
          type: string
          example: "series_1"
        action:
          type: string
          description: What the decision does (or would do) to the data.
          example: "null"
        before:
          type: object
          additionalProperties: true
          description: Summary of the affected data before the decision.
        after:
          type: object
          additionalProperties: true
          description: Summary of the affected data after the decision.
        reversible:
          type: boolean
          description: Whether `options.cleaning.decline` can undo it.
        evidence:
          type: object
          additionalProperties: true
          description: Measurements that triggered the decision.

    ForecastResponse:
      type: object
      required:
        - request_id
        - model_used
        - forecast
        - data_quality
        - usage
      properties:
        request_id:
          type: string
          example: "req_01J8K9XQW5ABCD1234"
        model_used:
          type: string
          description: Concrete public id of the model executed (`hora-1.0`, also when the request sent the alias `hora`)
          example: "hora-1.0"
        freq:
          type: string
          example: "1h"
        point_quantile:
          type: number
          x-beta: true
          description: |
            Present only when the request sent `point_quantile`: the native quantile actually
            served as `y_hat` (differs from the request when it was substituted under
            `quantile_policy: nearest`).
          example: 0.7
        forecast:
          type: array
          items:
            $ref: "#/components/schemas/ForecastResultPoint"
        data_quality:
          $ref: "#/components/schemas/DataQualityReport"
        usage:
          $ref: "#/components/schemas/UsageMetrics"
        cleaning_report:
          type: object
          x-beta: true
          additionalProperties: true
          description: |
            Beta: structured cleaning report (schema_version cleaning_report/1). Shape may change
            without a major version. Per series it lists every automatic decision: the `imputed`
            action carries one entry per filled gap (`gaps[]` with range, `method`, evidence `tier`
            `T0`–`T3` and `reason`), a `dropped` action with `reason: long_gap_split` names history
            removed by the segment split, and a `timezone` block states how offset-less timestamps were read.
            From policy version `2026-11` on, each gap additionally carries the `constraints` that
            changed its fills, `series[].seasonality` states the seasonal period that was
            tested (`period`, `source`, `acf`, `cycles`, `accepted`, `reason`), and new decisions
            and issues may appear: `constraint.nonnegative`, `constraint.bounds`,
            `constraint.structural_zero` (fabricate, declinable), `grid.business_days`
            (structural, declinable), and the issues `business_days_pattern`,
            `business_days_not_applicable`, `negative_in_nonnegative`, `out_of_bounds`,
            `thousands_separator`, `decimal_comma`, `ambiguous_separator`, `number_format_mixed`,
            `non_numeric_value`, `constant_series`, `all_zero_series`. Version `2026-12` adds
            the decision `gap.masked_tail` (fabricate; declining it blocks the request), the
            issue and warning `masked_tail`, and the blocking reason `trailing_gap_too_long`
            (422 `SERIES_DISCONTINUOUS`). Version `2026-13` adds the issue and warning
            `missing_pattern` (with `reasons`), `reason` / `reasons` on the `health_critical`
            blocking entry, and `series[].health.factors`: the deductions (`code`, `points`,
            optional `count` and `ratio`) whose points sum to 100 minus the health score.
          properties:
            schema_version:
              type: string
              example: "cleaning_report/1"
            policy:
              type: object
              additionalProperties: true
              description: The resolved cleaning policy; `id` names the default rule set and its version.
              properties:
                id:
                  type: string
                  example: "default@2026-13"
            decisions:
              type: array
              description: Every data-changing decision, plus proposals that were not applied.
              items:
                $ref: "#/components/schemas/CleaningDecision"
        effective_policy:
          $ref: "#/components/schemas/EffectivePolicy"
        model_input:
          type: array
          x-beta: true
          description: |
            Beta: what each entity's model actually received after model-specific adaptation.
            `truncated` is always 0: a series longer than the model's context is rejected
            (`SERIES_TOO_LONG`), never cut.
          items:
            $ref: "#/components/schemas/ModelInputReport"
        quantile_report:
          $ref: "#/components/schemas/QuantileReport"
        warnings:
          type: array
          x-beta: true
          description: |
            Beta: notices about this forecast that did not stop it; omitted when there are none.
            Read them before using the forecast. Codes may be added without a major version.
          items:
            $ref: "#/components/schemas/ForecastWarning"

    ForecastWarning:
      type: object
      x-beta: true
      required:
        - code
        - message
      description: |
        Beta. A notice about one entity's forecast, or about the whole request when `unique_id`
        is absent. `trailing_values_missing` (cleaning policy
        `2026-12` and later): the last `count` input values were empty and were not used as data; see
        `tail_handling` in that entity's `model_input` entry. Forecast timestamps still start one
        step after the last input timestamp. `impact` is a qualitative class (more missing values,
        and more of them relative to `h`, mean a larger expected effect on the forecast), and
        `confirmed: true` marks a run longer than 3 steps accepted through
        `options.cleaning.trailing_gap: extend`. `long_gap_mask_unavailable`
        (`options.cleaning.long_gap: mask`): the model cannot mask missing values, so it read the
        filled values of `count` long-gap positions (`model_input[].long_gap_handling: fill`).
        `cleaning_version_deprecated` (whole request, also on `/v1/clean`): the request, API Key
        or organization setting pinned the deprecated cleaning policy `version`; it was applied
        unchanged, but will be removed after notice. Omit `options.cleaning.version` (and clear the
        key / organization setting) to follow the platform default.
      properties:
        code:
          type: string
          enum: [trailing_values_missing, long_gap_mask_unavailable, cleaning_version_deprecated]
          example: trailing_values_missing
        unique_id:
          type: string
          example: "meter-001"
        count:
          type: integer
          minimum: 1
          description: |
            Trailing empty steps (`trailing_values_missing`) or long-gap positions read as filled
            values (`long_gap_mask_unavailable`).
          example: 1
        tail_handling:
          type: string
          enum: [mask, truncate_extend, two_pass]
          description: Same as `model_input[].tail_handling` for this entity (`trailing_values_missing` only).
          example: truncate_extend
        impact:
          type: string
          enum: [low, medium, high]
          description: |
            `trailing_values_missing` only. Qualitative expected effect of the missing trailing
            values on this forecast: `low` for up to 3 values; `medium` or `high` for longer runs,
            `high` when many values are missing or they make up more than half of `h`. It is not
            an accuracy estimate.
          example: low
        confirmed:
          type: boolean
          description: |
            `trailing_values_missing` only; present (`true`) when more than 3 trailing values were
            missing and the request accepted them with `options.cleaning.trailing_gap: extend`.
          example: true
        version:
          type: string
          description: The deprecated cleaning policy version that was applied (`cleaning_version_deprecated` only).
          example: "2026-10"
        message:
          type: string
          description: Human-readable English summary; do not parse it.
          example: "Series meter-001: the last value was missing; the model treated the missing positions as unknown. Forecast timestamps still start one step after the last input timestamp."

    QuantileReport:
      type: object
      x-beta: true
      required:
        - policy
        - requested
        - used
        - substitutions
      description: |
        Beta. How the quantiles behind `intervals` (and `y_hat` with `point_quantile`) were
        served. The model only produces its native quantiles; they are never interpolated.
      properties:
        policy:
          type: string
          enum: [nearest, strict]
          example: nearest
        requested:
          type: array
          description: Quantiles implied by `level`, plus `point_quantile` when sent, plus the median, sorted.
          items:
            type: number
          example: [0.025, 0.5, 0.975]
        used:
          type: array
          description: Native quantiles actually used, sorted.
          items:
            type: number
          example: [0.1, 0.5, 0.9]
        substitutions:
          type: array
          description: One entry per requested quantile the model does not produce; empty when none.
          items:
            type: object
            required:
              - requested
              - used
            properties:
              requested:
                type: number
                example: 0.025
              used:
                type: number
                example: 0.1
        point_forecast:
          type: object
          description: Present only when the request sent `point_quantile`. Which quantile `y_hat` carries.
          required:
            - requested
            - used
            - method
          properties:
            requested:
              type: number
              example: 0.75
            used:
              type: number
              example: 0.8
            method:
              type: string
              enum: [native, nearest]
              description: "`native`: the model produces the requested quantile; `nearest`: substituted."
              example: nearest

    EffectivePolicy:
      type: object
      x-beta: true
      required:
        - id
        - preset
        - version
        - source
      description: Beta. The cleaning policy that governed this request and where it came from.
      properties:
        id:
          type: string
          description: "`<preset>@<version>`."
          example: "default@2026-13"
        preset:
          type: string
          enum: [default, strict, raw]
          example: default
        version:
          type: string
          description: Rule-set version the cleaning service applied (`2026-13` unless another version was selected).
          example: "2026-13"
        source:
          type: string
          enum: [request, key, org, platform]
          description: |
            The highest-priority layer that set the preset or the version (`request` when the
            request sets either).
          example: platform

    ModelInputReport:
      type: object
      x-beta: true
      required:
        - unique_id
        - context_points
        - fed_points
        - truncated
        - missing_mode
        - covariates_used
      properties:
        unique_id:
          type: string
          example: "meter-001"
        context_points:
          type: integer
          minimum: 0
          description: Points handed to the model.
          example: 24
        fed_points:
          type: integer
          minimum: 0
          description: Observed points among them (the billed input points).
          example: 23
        truncated:
          type: integer
          minimum: 0
          description: |
            Oldest steps dropped to fit the model's context window. Always 0: longer series are
            rejected with `SERIES_TOO_LONG` before inference.
          example: 0
        missing_mode:
          type: string
          enum: [filled, native]
          description: "`filled`: cleaned (filled) values were fed; `native`: the model masked missing positions itself."
          example: filled
        covariates_used:
          type: boolean
          description: True when at least one covariate column reached the model.
          example: false
        masked_tail:
          type: integer
          minimum: 1
          description: |
            Trailing empty steps after the last observed point that were kept on the grid
            (cleaning policy `2026-12` and later) and not used as data; omitted when there were none.
            They are not counted in `fed_points` and not billed.
          example: 1
        tail_handling:
          type: string
          enum: [mask, truncate_extend, two_pass]
          description: |
            How the model handled `masked_tail`; omitted when there was none. Usually
            `truncate_extend`: the model forecast from the last observed point and the first
            `masked_tail` predicted steps were dropped, so the returned stamps still start right
            after the last input stamp. `two_pass`: `h + masked_tail` exceeded what the model
            forecasts in one call, so it first forecast the `masked_tail` missing steps from the
            last observed point, used that forecast in their place, and then forecast the `h`
            steps; the first pass is not billed. `mask` (the positions handed to the model as
            missing values) is reserved and not currently used. The forecast covers exactly the
            requested `h` steps after the last input timestamp; a `trailing_values_missing`
            warning names the entity.
          example: truncate_extend
        long_gap_points:
          type: integer
          minimum: 1
          description: |
            With `options.cleaning.long_gap: mask`: positions of long interior gaps inside the
            context handed to the model; omitted when there were none (or `long_gap` was not
            `mask`). They are not counted in `fed_points` and not billed.
          example: 12
        long_gap_handling:
          type: string
          enum: [mask, fill]
          description: |
            How the model read `long_gap_points`; omitted with them. `mask`: as missing values.
            `fill`: the model cannot mask missing values, so it read cleaning's filled values
            (`long_gap_reason` says why and a `long_gap_mask_unavailable` warning names the
            entity).
          example: mask
        long_gap_reason:
          type: string
          enum: [model_without_mask]
          description: Why `long_gap_handling` is `fill` although `mask` was requested; omitted otherwise.
          example: model_without_mask
        covariates:
          type: array
          description: |
            Beta: one entry per covariate column sent for this entity; omitted when none was sent.
          items:
            $ref: "#/components/schemas/CovariateReport"

    CovariateReport:
      type: object
      x-beta: true
      required:
        - name
        - role
        - used
      description: |
        Whether one covariate column reached the model. `role` is `future` for columns also given
        in `future_covariates`, otherwise `past`. Reasons when unused: `non_numeric`,
        `data_incomplete` (long gaps, or future values not covering the horizon), `raw_preset`
        (the raw preset does not align covariates), `model_unsupported`, `role_unsupported` (a
        future column on a model that only takes past covariates), `limit_exceeded` (beyond the
        model's covariate limit).
      properties:
        name:
          type: string
          example: "temperature"
        role:
          type: string
          enum: [past, future]
        used:
          type: boolean
        reason:
          type: string
          enum: [non_numeric, data_incomplete, raw_preset, model_unsupported, role_unsupported, limit_exceeded]

    CleanRequest:
      type: object
      x-beta: true
      required:
        - series
      properties:
        series:
          type: array
          description: Historical flat long-table records
          items:
            $ref: "#/components/schemas/TimeSeriesPoint"
        freq:
          $ref: "#/components/schemas/ForecastRequest/properties/freq"
        clean_series:
          $ref: "#/components/schemas/ForecastRequest/properties/clean_series"
        model:
          type: string
          description: |
            Optional model id (`hora-1.0` or the alias `hora`; any other id is 422
            `MODEL_NOT_AVAILABLE`); when given, that model's minimum length is applied to the report
            and a series longer than its maximum context is rejected with 422 `SERIES_TOO_LONG`.
          example: "hora-1.0"
        options:
          $ref: "#/components/schemas/ForecastRequest/properties/options"

    CleanResponse:
      type: object
      x-beta: true
      required:
        - request_id
        - freq
        - cleaning_report
        - effective_policy
      properties:
        request_id:
          type: string
          example: "req_01J8K9XQW5ABCD1234"
        freq:
          type: string
          example: "h"
        cleaning_report:
          $ref: "#/components/schemas/ForecastResponse/properties/cleaning_report"
        effective_policy:
          $ref: "#/components/schemas/EffectivePolicy"
        warnings:
          type: array
          x-beta: true
          description: |
            Beta: notices about this request that did not stop it (today only
            `cleaning_version_deprecated`); omitted when there are none.
          items:
            $ref: "#/components/schemas/ForecastWarning"

    DataQualityReport:
      type: object
      required:
        - status
        - missing_ratio
      properties:
        status:
          type: string
          enum: [ok, cleaned, rejected]
          description: |
            ok — input already regular; cleaned — repairs applied; rejected — surfaced via error
            responses rather than a 200 body (field reserved for partial batch semantics later).
          example: cleaned
        inferred_freq:
          type:
            - string
            - "null"
          description: Frequency inferred when request.freq was omitted
          example: "1h"
        missing_ratio:
          type: number
          minimum: 0
          maximum: 1
          description: Fraction of expected timestamps missing before imputation
          example: 0.04
        imputed_points:
          type: integer
          minimum: 0
          description: Count of y values filled by the cleaner
          example: 1
        outlier_points_suppressed:
          type: integer
          minimum: 0
          description: Count of spike values winsorized or masked
          example: 0
        timezone_normalized:
          type: boolean
          description: Whether timestamps were normalized to a consistent UTC offset
          example: true
        notes:
          type: array
          items:
            type: string
          description: Human-readable audit lines for operators
          example:
            - "1 missing y filled via local continuity interpolation"

    AnomalyDetectionRequest:
      type: object
      required:
        - series
      properties:
        model:
          type: string
          default: "hora-1.0"
        series:
          type: array
          items:
            $ref: "#/components/schemas/TimeSeriesPoint"
        confidence:
          type: integer
          minimum: 80
          maximum: 99
          default: 99
          description: Confidence threshold percentage for anomaly classification
        freq:
          type: string
          example: "1h"

    AnomalyResultPoint:
      type: object
      required:
        - unique_id
        - ds
        - y
        - is_anomaly
        - anomaly_score
      properties:
        unique_id:
          type: string
        ds:
          type: string
        y:
          type:
            - number
            - "null"
        expected_value:
          type: number
        is_anomaly:
          type: boolean
          description: True if observation falls outside dynamic threshold bounds
        anomaly_score:
          type: number
          minimum: 0.0
          maximum: 1.0
          description: Normalized severity score of the anomaly
        threshold_lo:
          type: number
        threshold_hi:
          type: number

    AnomalyDetectionResponse:
      type: object
      required:
        - request_id
        - anomalies
        - usage
      properties:
        request_id:
          type: string
        anomalies:
          type: array
          items:
            $ref: "#/components/schemas/AnomalyResultPoint"
        usage:
          $ref: "#/components/schemas/UsageMetrics"

    # -------------------------------------------------------------
    # Batch Job Schemas
    # -------------------------------------------------------------
    BatchForecastRequest:
      type: object
      required:
        - input_file_url
        - h
        - format
      properties:
        model:
          type: string
          default: "hora-1.0"
        input_file_url:
          type: string
          format: uri
          description: Pre-signed OSS/S3 URL to the dataset file
          example: "https://oss.mycorp.com/data/timeseries_batch_20260917.parquet"
        format:
          type: string
          enum: [csv, parquet]
          default: parquet
        h:
          type: integer
          example: 24
        freq:
          type: string
          example: "1h"
        level:
          type: array
          items:
            type: integer
          example: [80, 95]
        point_quantile:
          type: number
          minimum: 0.01
          maximum: 0.99
          description: |
            Same as `ForecastRequest.point_quantile` under `quantile_policy: nearest`: the
            quantile written as `y_hat` in the result file instead of the median (0.01 to 0.99,
            at most 3 decimal places, served by the nearest native quantile).
          example: 0.7

    BatchJobSummary:
      type: object
      required:
        - job_id
        - status
        - created_at
      properties:
        job_id:
          type: string
          example: "job_01J8K9XQW5ABCD1234"
        status:
          type: string
          enum: [SUBMITTED, VALIDATING, PROCESSING, SUCCEEDED, FAILED, CANCELLED]
          example: "SUBMITTED"
        created_at:
          type: string
          format: date-time
          example: "2026-09-17T07:28:00Z"

    BatchJobDetail:
      allOf:
        - $ref: "#/components/schemas/BatchJobSummary"
        - type: object
          properties:
            progress_percentage:
              type: integer
              minimum: 0
              maximum: 100
              example: 45
            processed_series_count:
              type: integer
              example: 25000
            total_series_count:
              type: integer
              example: 50000
            output_file_url:
              type: string
              format: uri
              description: Pre-signed download link to compressed result Parquet/CSV
            error_details:
              type: string
              nullable: true

    UsageMetrics:
      type: object
      required:
        - series_count
        - input_data_points
        - latency_ms
      properties:
        series_count:
          type: integer
          description: Count of distinct unique_id entities processed
          example: 1
        input_data_points:
          type: integer
          description: |
            Observed historical points actually fed to the model: counted after the segment split,
            excluding imputed values and inserted grid stamps.
          example: 24
        forecast_data_points:
          type: integer
          description: Total number of future points generated
          example: 24
        latency_ms:
          type: integer
          description: End-to-end inference compute duration in milliseconds
          example: 118

    # -------------------------------------------------------------
    # Error Model Schema
    # -------------------------------------------------------------
    ErrorDetail:
      type: object
      description: |
        One error. Branch on `code` (or on the coarser `category`); never parse `message`.
        A 422 means the request was parsed but its content cannot be used as sent.
        `x-error-codes` lists every code the public API returns with its HTTP status,
        `category` and `retryable`; a code missing from that list is grouped by its status
        (5xx `server`, 429 `rate_limited`, 409 `conflict`, 401/403 `auth`, other 4xx
        `invalid_request`).
      required:
        - code
        - message
        - request_id
      properties:
        code:
          type: string
          description: Machine-readable application error code
          example: "SERIES_DISCONTINUOUS"
        category:
          type: string
          description: |
            Coarse group of `code`, always sent by the public API. Values:
            `invalid_request` (malformed JSON, schema or header validation, cleaning options,
            idempotency key reuse), `data_invalid` (the data cannot be cleaned or modelled as sent),
            `limit_exceeded` (the request is larger than a cap or the model's capacity: rows,
            series length, horizon), `model_unavailable` (the model or a model option or quantile
            cannot be used), `auth` (401 / 403), `conflict` (the same idempotency key is still
            running), `rate_limited` (429), `server` (500 / 503 / 504 including `MODEL_BUSY`).
            New values may be added; treat an unknown value like `server` if `retryable` is true
            and like `invalid_request` otherwise.
          x-extensible-enum:
            - invalid_request
            - data_invalid
            - limit_exceeded
            - model_unavailable
            - auth
            - conflict
            - rate_limited
            - server
          example: "data_invalid"
        retryable:
          type: boolean
          description: |
            Whether the same request may succeed if sent again unchanged (after `Retry-After`
            when present). True for 409 `IDEMPOTENCY_IN_PROGRESS`, 429, 500, 503 (including
            `MODEL_BUSY`) and 504; false for everything else. A daily-quota 429 is retryable only
            after the next UTC midnight. Always sent by the public API.
          example: false
        message:
          type: string
          description: Human-readable error description
          example: "Severe timestamp discontinuity detected; over 30% points missing."
        request_id:
          type: string
          example: "req_01J8K9XQW5ABCD1234"
        details:
          type: object
          additionalProperties: true
          description: Supplementary diagnostic attributes
      x-error-codes:
        - {code: MALFORMED_JSON, status: [400], category: invalid_request, retryable: false}
        - {code: VALIDATION_ERROR, status: [400, 422], category: invalid_request, retryable: false}
        - {code: IDEMPOTENCY_KEY_REUSED, status: [422], category: invalid_request, retryable: false}
        - {code: CLEANING_OPTION_UNSUPPORTED, status: [422], category: invalid_request, retryable: false}
        - {code: IDEMPOTENCY_IN_PROGRESS, status: [409], category: conflict, retryable: true}
        - {code: INVALID_API_KEY, status: [401], category: auth, retryable: false}
        - {code: INSUFFICIENT_SCOPE, status: [403], category: auth, retryable: false}
        - {code: ACCOUNT_SUSPENDED, status: [403], category: auth, retryable: false}
        - {code: ORG_PENDING_DELETION, status: [403], category: auth, retryable: false}
        - {code: FUTURE_COVARIATES_INVALID, status: [422], category: data_invalid, retryable: false}
        - {code: COVARIATE_COLUMN_MISMATCH, status: [422], category: data_invalid, retryable: false}
        - {code: RAW_INPUT_INVALID, status: [422], category: data_invalid, retryable: false}
        - {code: SERIES_TOO_SHORT, status: [422], category: data_invalid, retryable: false}
        - {code: SERIES_DISCONTINUOUS, status: [422], category: data_invalid, retryable: false}
        - {code: TIMESTAMP_IRREGULAR, status: [422], category: data_invalid, retryable: false}
        - {code: FREQ_INFERENCE_FAILED, status: [422], category: data_invalid, retryable: false}
        - {code: FREQ_AMBIGUOUS, status: [422], category: data_invalid, retryable: false}
        - {code: FREQ_UNSUPPORTED, status: [422], category: data_invalid, retryable: false}
        - {code: SERIES_EMPTY, status: [422], category: data_invalid, retryable: false}
        - {code: AMBIGUOUS_DATE_FORMAT, status: [422], category: data_invalid, retryable: false}
        - {code: POINTS_EXCEEDED, status: [422], category: limit_exceeded, retryable: false}
        - {code: SERIES_TOO_LONG, status: [422], category: limit_exceeded, retryable: false}
        - {code: HORIZON_EXCEEDS_MODEL, status: [422], category: limit_exceeded, retryable: false}
        - {code: MODEL_NOT_AVAILABLE, status: [422], category: model_unavailable, retryable: false}
        - {code: MODEL_OPTION_UNSUPPORTED, status: [422], category: model_unavailable, retryable: false}
        - {code: QUANTILE_UNSUPPORTED, status: [422], category: model_unavailable, retryable: false}
        - {code: RATE_LIMIT_EXCEEDED, status: [429], category: rate_limited, retryable: true}
        - {code: SERVICE_UNAVAILABLE, status: [503], category: server, retryable: true}
        - {code: MODEL_BUSY, status: [503], category: server, retryable: true}
        - {code: GATEWAY_TIMEOUT, status: [504], category: server, retryable: true}
        - {code: INTERNAL_SERVER_ERROR, status: [500], category: server, retryable: true}

    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/ErrorDetail"

  headers:
    RequestId:
      description: Server-generated request id (`req_…`); quote it when contacting support.
      schema:
        type: string
        example: "req_01J8K9XQW5ABCD1234"
    ClientRequestId:
      description: Echo of the caller's `X-Request-Id` when it matched the accepted pattern.
      schema:
        type: string
        example: "c79439f0-281b-4f9a-9e7f-7128a38a7c21"
    RateLimitLimit:
      description: |
        Ceiling of the bucket named by `X-RateLimit-Limit-Type`: requests per minute for `rpm`,
        points per day for `daily`. Both buckets belong to the account: every API Key of one
        account shares the same window and the same daily total. Sent on every response once
        the API Key is authenticated (success and errors alike); not sent when the key is rejected.
      schema:
        type: integer
        example: 10
    RateLimitRemaining:
      description: |
        Room left in that bucket after this request: requests in the current 60-second sliding
        window for `rpm`, points left today for `daily`.
      schema:
        type: integer
        example: 9
    RateLimitReset:
      description: |
        Unix time (seconds) when the bucket next has room: when the oldest counted request leaves
        the window for `rpm`, the next UTC midnight for `daily`.
      schema:
        type: integer
        example: 1789632000
    RateLimitLimitType:
      description: |
        Which bucket the other `X-RateLimit-*` headers describe. Non-429 responses report the
        account's request limit (`rpm`, shared by all its keys); a 429 reports the bucket that was exhausted (`rpm` or `daily`).
      schema:
        type: string
        enum: [rpm, daily]
        example: "rpm"
    RateLimitPolicy:
      description: |
        Policy identifier of that bucket: `forecast-rpm`, `clean-rpm` or `forecast-daily-points`.
      schema:
        type: string
        example: "forecast-rpm"
    IdempotentReplayed:
      description: |
        `true` when this response is the stored outcome of an earlier request with the same
        `Idempotency-Key` and body (nothing ran or was billed again); absent otherwise.
        `X-Request-Id` and the body's `request_id` are the original request's.
      schema:
        type: string
        enum: ["true"]
    IdempotencyKeyAliasDeprecation:
      description: |
        Present when the request sent only the deprecated `X-Idempotency-Key` alias (on any
        status); switch to `Idempotency-Key`. Absent otherwise. RFC 9745 structured-field date:
        the alias has been deprecated since 2026-10-08T00:00:00Z.
      schema:
        type: string
        enum: ["@1791417600"]

  examples:
    malformed_json:
      summary: Unparseable JSON body
      value:
        error:
          code: "MALFORMED_JSON"
          message: "Failed to parse request JSON payload"
          request_id: "req_01J8K9XQW5ABCD1234"
    invalid_api_key:
      summary: Invalid or expired API Key
      value:
        error:
          code: "INVALID_API_KEY"
          message: "Provided API Key is invalid or expired"
          request_id: "req_01J8K9XQW5ABCD1234"
    insufficient_scope:
      summary: API Key missing required scope
      value:
        error:
          code: "INSUFFICIENT_SCOPE"
          message: "API key is not authorized for this operation"
          request_id: "req_01J8K9XQW5ABCD1234"
    ip_not_allowed:
      summary: Caller IP not on allowlist
      value:
        error:
          code: "IP_NOT_ALLOWED"
          message: "Request origin is not permitted for this API key"
          request_id: "req_01J8K9XQW5ABCD1234"
    account_suspended:
      summary: The key's account or workspace is suspended
      x-beta: true
      value:
        error:
          code: "ACCOUNT_SUSPENDED"
          message: "This account is suspended; contact SequenxAI support"
          request_id: "req_01J8K9XQW5ABCD1234"
    org_pending_deletion:
      summary: The key's workspace is in a deletion cool-off
      x-beta: true
      value:
        error:
          code: "ORG_PENDING_DELETION"
          message: "This workspace is pending deletion; API keys are disabled until cancelled"
          request_id: "req_01J8K9XQW5ABCD1234"
    idempotency_pending:
      summary: Same key still running
      value:
        error:
          code: "IDEMPOTENCY_IN_PROGRESS"
          message: "A request with this idempotency key is still in progress; retry later"
          request_id: "req_01J8K9XQW5ABCD1234"
    idempotency_body_mismatch:
      summary: Same Idempotency-Key, different body (routes that accept the header)
      value:
        error:
          code: "IDEMPOTENCY_KEY_REUSED"
          message: "Idempotency key was reused with a different request body"
          request_id: "req_01J8K9XQW5ABCD1234"
    future_covariates_invalid:
      summary: Future table contains target column
      value:
        error:
          code: "FUTURE_COVARIATES_INVALID"
          message: "future_covariates must not contain target column 'y'"
          request_id: "req_01J8K9XQW5ABCD1234"
    series_discontinuous:
      summary: Gaps too large to repair safely
      value:
        error:
          code: "SERIES_DISCONTINUOUS"
          message: "Severe timestamp discontinuity; missing ratio exceeds repair threshold"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            missing_ratio: 0.34
            threshold: 0.20
            unique_id: "meter-001"
    freq_inference_failed:
      summary: Cannot infer frequency
      value:
        error:
          code: "FREQ_INFERENCE_FAILED"
          message: "Unable to infer a stable Pandas frequency; pass freq explicitly"
          request_id: "req_01J8K9XQW5ABCD1234"
    timestamp_irregular:
      summary: Irregular sampling without clean_series
      value:
        error:
          code: "TIMESTAMP_IRREGULAR"
          message: "Irregular timestamps detected; enable clean_series or resample client-side"
          request_id: "req_01J8K9XQW5ABCD1234"
    ambiguous_date_format:
      summary: Day/month order cannot be determined
      value:
        error:
          code: "AMBIGUOUS_DATE_FORMAT"
          message: "Day/month order cannot be determined from the data; pass options.ds_format ('dmy' or 'mdy')"
          request_id: "req_01J8K9XQW5ABCD1234"
    points_exceeded:
      summary: Request exceeds the per-request data point cap
      value:
        error:
          code: "POINTS_EXCEEDED"
          message: "Request exceeds the per-request data point cap"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            points: 240000
            cap: 200000
    freq_unsupported:
      summary: Frequency string or inferred step is unsupported
      value:
        error:
          code: "FREQ_UNSUPPORTED"
          message: "Sampling frequency is unsupported"
          request_id: "req_01J8K9XQW5ABCD1234"
    freq_ambiguous:
      summary: Several candidate frequencies
      value:
        error:
          code: "FREQ_AMBIGUOUS"
          message: "Sampling frequency is ambiguous; pass freq explicitly"
          request_id: "req_01J8K9XQW5ABCD1234"
    series_too_short:
      summary: Fewer points than the model minimum context
      value:
        error:
          code: "SERIES_TOO_SHORT"
          message: "Series shorter than model minimum context after cleaning"
          request_id: "req_01J8K9XQW5ABCD1234"
    series_too_long:
      summary: An entity is longer than the selected model's maximum context
      description: |
        Measured per `unique_id` on the points the model would receive: rows under the `raw`
        preset, distinct timestamps before cleaning, the regular grid after cleaning
        (`details.measured`: `rows`, `timestamps` or `model_input`). An entity over the limit
        before cleaning that has a gap at or above the segment-split threshold (see
        `options.gap_split_min_steps`) is not rejected there: cleaning may keep only its latest
        segment, and the check after cleaning decides. At most 10 entities are
        listed, longest first; `series_count` counts all of them. Nothing is billed.
      value:
        error:
          code: "SERIES_TOO_LONG"
          message: "A series is longer than the maximum context of model 'hora-1.0' (4096 points); send only the most recent 4096 points per series"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "hora-1.0"
            max_context: 4096
            measured: "timestamps"
            series:
              - unique_id: "meter-001"
                points: 8760
            series_count: 1
            hint: "Send only the most recent 4096 points of each listed series"
    horizon_exceeds_model:
      summary: Horizon larger than the model's maximum
      value:
        error:
          code: "HORIZON_EXCEEDS_MODEL"
          message: "h=1500 exceeds the maximum horizon 1024 of model 'hora-1.0'"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "hora-1.0"
            h: 1500
            max_horizon: 1024
    quantile_unsupported:
      summary: quantile_policy strict and a level the model cannot produce natively
      value:
        error:
          code: "QUANTILE_UNSUPPORTED"
          message: "Model 'hora-1.0' does not natively produce quantile levels [0.025, 0.975]"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "hora-1.0"
            level: [95]
            unsupported: [0.025, 0.975]
            supported: [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99]
    point_quantile_unsupported:
      summary: quantile_policy strict and a point_quantile the model cannot produce natively
      value:
        error:
          code: "QUANTILE_UNSUPPORTED"
          message: "Model 'hora-1.0' does not natively produce quantile levels [0.33]"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "hora-1.0"
            level: []
            unsupported: [0.33]
            supported: [0.01, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99]
            point_quantile: 0.33
    model_not_available:
      summary: A model id other than hora-1.0 / hora
      description: |
        `hora-1.0` (alias `hora`) is the only public model. Any other id is rejected before any
        quota is reserved; nothing is billed. `details.available` lists the accepted ids.
      value:
        error:
          code: "MODEL_NOT_AVAILABLE"
          message: "Model 'my-model' is not available; use 'hora-1.0'"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "my-model"
            available: ["hora", "hora-1.0"]
    model_option_unsupported:
      summary: A model_options key the selected model does not take
      description: |
        `hora-1.0` takes every `model_options` key, so this does not occur with it today; a model
        that takes fewer options answers this way, before any quota is reserved. `details.model`
        names that model's public id.
      value:
        error:
          code: "MODEL_OPTION_UNSUPPORTED"
          message: "Model '<model>' does not take model_options [flip_average]"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            model: "<model>"
            unsupported: ["flip_average"]
            supported: ["nonnegative"]
    series_empty:
      summary: No usable numeric values
      value:
        error:
          code: "SERIES_EMPTY"
          message: "Entity contains no numeric y values"
          request_id: "req_01J8K9XQW5ABCD1234"
    duplicate_timestamps:
      summary: Duplicate timestamps rejected by duplicate policy
      value:
        error:
          code: "DUPLICATE_TIMESTAMPS"
          message: "Duplicate timestamps rejected by policy"
          request_id: "req_01J8K9XQW5ABCD1234"
    cleaning_option_unsupported:
      summary: Cleaning option value not implemented
      value:
        error:
          code: "CLEANING_OPTION_UNSUPPORTED"
          message: "Cleaning option value is not implemented yet"
          request_id: "req_01J8K9XQW5ABCD1234"
    cleaning_option_requires_version:
      summary: A cleaning knob needs policy version 2026-11
      value:
        error:
          code: "CLEANING_OPTION_UNSUPPORTED"
          message: "Cleaning options require policy version 2026-11"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            options: ["business_days"]
            policy: "default@2026-10"
            requires_version: "2026-11"
    validation_error:
      summary: Payload or option failed schema validation
      value:
        error:
          code: "VALIDATION_ERROR"
          message: "Request payload failed schema validation"
          request_id: "req_01J8K9XQW5ABCD1234"
    cleaning_option_unknown:
      summary: Unknown field in options.cleaning
      value:
        error:
          code: "VALIDATION_ERROR"
          message: "Request payload failed schema validation"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            errors:
              - field: "options.cleaning.imputer"
                message: "Extra inputs are not permitted"
    raw_input_invalid:
      summary: Raw preset input is not a regular, finite series
      value:
        error:
          code: "RAW_INPUT_INVALID"
          message: "Raw input is not a regular, finite series"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            violations:
              - unique_id: "meter-001"
                index: 5
                reason: "step_mismatch"
            hint: "Use the default cleaning preset, or call /v1/clean to see the cleaning report."
    covariate_column_mismatch:
      summary: Future covariate columns have no history
      description: |
        Only `future_only` columns are an error: a future-known column needs its history.
        Columns present only in the history are valid past-only covariates.
      value:
        error:
          code: "COVARIATE_COLUMN_MISMATCH"
          message: "future_covariates columns must also appear in the history"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            future_only: ["forecast_temperature"]
    covariate_limit_exceeded:
      summary: More covariate columns than allowed
      value:
        error:
          code: "VALIDATION_ERROR"
          message: "At most 8 covariate columns are accepted"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            field: "series"
            covariates: 9
            max_covariates: 8
    rate_limit_exceeded:
      summary: Per-minute limit hit
      value:
        error:
          code: "RATE_LIMIT_EXCEEDED"
          message: "Too many requests for this account; please respect the Retry-After interval"
          request_id: "req_01J8K9XQW5ABCD1234"
          details:
            limit_type: "rpm"
            limit_scope: "account"
            limit_value: 60
            retry_after_seconds: 5

  responses:
    BadRequestError:
      description: |
        Invalid payload syntax or unparseable JSON (`MALFORMED_JSON`), or an `Idempotency-Key`
        that is not 1–255 printable ASCII characters or disagrees with the deprecated
        `X-Idempotency-Key` alias (`VALIDATION_ERROR`, `details.field` names the header).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            malformed_json:
              $ref: "#/components/examples/malformed_json"

    UnauthorizedError:
      description: Authentication failed or API Key missing, deleted or revoked
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            invalid_api_key:
              $ref: "#/components/examples/invalid_api_key"

    ForbiddenError:
      description: |
        Authenticated caller lacks permission (scope or IP allowlist), the key's account or
        workspace is suspended by SequenxAI (`ACCOUNT_SUSPENDED`, beta), or the workspace is in a deletion
        cool-off (`ORG_PENDING_DELETION`, beta). A suspended account's or workspace's keys stay valid and
        work again once it is restored. Not billed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            insufficient_scope:
              $ref: "#/components/examples/insufficient_scope"
            ip_not_allowed:
              $ref: "#/components/examples/ip_not_allowed"
            account_suspended:
              $ref: "#/components/examples/account_suspended"
            org_pending_deletion:
              $ref: "#/components/examples/org_pending_deletion"

    IdempotencyInProgressError:
      description: |
        `IDEMPOTENCY_IN_PROGRESS`: a request with the same `Idempotency-Key` is still running;
        retry after it finishes to receive the stored result. Replays with an identical body
        return the original result without double-charging usage.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            idempotency_pending:
              $ref: "#/components/examples/idempotency_pending"

    ValidationError:
      description: |
        Business validation or cleaning failure (e.g. missing ds column, future table contains y,
        discontinuity beyond repair threshold, frequency inference failure). On routes that accept
        `Idempotency-Key`, also `IDEMPOTENCY_KEY_REUSED`: the same key was sent with a different
        request body within the 24-hour window (nothing ran or was billed). A `model` other than
        `hora-1.0` / `hora` is `MODEL_NOT_AVAILABLE`; a `model_options` key the model does not take
        is `MODEL_OPTION_UNSUPPORTED`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            model_not_available:
              $ref: "#/components/examples/model_not_available"
            model_option_unsupported:
              $ref: "#/components/examples/model_option_unsupported"
            idempotency_body_mismatch:
              $ref: "#/components/examples/idempotency_body_mismatch"
            future_covariates_invalid:
              $ref: "#/components/examples/future_covariates_invalid"
            series_discontinuous:
              $ref: "#/components/examples/series_discontinuous"
            freq_inference_failed:
              $ref: "#/components/examples/freq_inference_failed"
            timestamp_irregular:
              $ref: "#/components/examples/timestamp_irregular"
            ambiguous_date_format:
              $ref: "#/components/examples/ambiguous_date_format"
            points_exceeded:
              $ref: "#/components/examples/points_exceeded"
            freq_unsupported:
              $ref: "#/components/examples/freq_unsupported"
            freq_ambiguous:
              $ref: "#/components/examples/freq_ambiguous"
            series_too_short:
              $ref: "#/components/examples/series_too_short"
            series_too_long:
              $ref: "#/components/examples/series_too_long"
            horizon_exceeds_model:
              $ref: "#/components/examples/horizon_exceeds_model"
            quantile_unsupported:
              $ref: "#/components/examples/quantile_unsupported"
            series_empty:
              $ref: "#/components/examples/series_empty"
            duplicate_timestamps:
              $ref: "#/components/examples/duplicate_timestamps"
            cleaning_option_unsupported:
              $ref: "#/components/examples/cleaning_option_unsupported"
            cleaning_option_requires_version:
              $ref: "#/components/examples/cleaning_option_requires_version"
            validation_error:
              $ref: "#/components/examples/validation_error"
            cleaning_option_unknown:
              $ref: "#/components/examples/cleaning_option_unknown"
            raw_input_invalid:
              $ref: "#/components/examples/raw_input_invalid"
            covariate_column_mismatch:
              $ref: "#/components/examples/covariate_column_mismatch"
            covariate_limit_exceeded:
              $ref: "#/components/examples/covariate_limit_exceeded"

    NotFoundError:
      description: Requested resource or job was not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: "JOB_NOT_FOUND"
              message: "Batch job with specified ID does not exist"
              request_id: "req_01J8K9XQW5ABCD1234"

    RateLimitError:
      description: |
        The account's per-minute request limit was exceeded or its daily point quota is
        exhausted. Limits belong to the account, not to the key: all keys of one account share
        them, and `details.limit_scope` is `account`.
      headers:
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
            example: 5
        X-RateLimit-Limit:
          $ref: "#/components/headers/RateLimitLimit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/RateLimitRemaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/RateLimitReset"
        X-RateLimit-Limit-Type:
          $ref: "#/components/headers/RateLimitLimitType"
        X-RateLimit-Policy:
          $ref: "#/components/headers/RateLimitPolicy"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            rate_limit_exceeded:
              $ref: "#/components/examples/rate_limit_exceeded"

    InternalServerError:
      description: Unexpected internal server or model runtime error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: "INTERNAL_SERVER_ERROR"
              message: "An internal error occurred during tensor inference"
              request_id: "req_01J8K9XQW5ABCD1234"

    ServiceUnavailableError:
      description: |
        Temporarily unable to serve: the model is busy (`MODEL_BUSY`, with `Retry-After`) or an
        internal stage is unavailable or paused by SequenxAI (`SERVICE_UNAVAILABLE`, with
        `Retry-After` when paused or when the shared cleaning capacity is full). Not billed; retry later.
      headers:
        Retry-After:
          description: Seconds to wait before retrying
          schema:
            type: integer
            example: 2
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            model_busy:
              summary: Model is busy
              value:
                error:
                  code: "MODEL_BUSY"
                  message: "Model is busy; retry after the indicated interval"
                  request_id: "req_01J8K9XQW5ABCD1234"
            service_unavailable:
              summary: An internal stage is unavailable
              value:
                error:
                  code: "SERVICE_UNAVAILABLE"
                  message: "Service temporarily unavailable; please retry"
                  request_id: "req_01J8K9XQW5ABCD1234"

    GatewayTimeoutError:
      description: Inference did not finish within the request budget. Not billed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: "GATEWAY_TIMEOUT"
              message: "Inference did not finish within the time budget"
              request_id: "req_01J8K9XQW5ABCD1234"
