WorkbookAPI

API reference

Base URL: https://api.workbookapi.com. Send keys only as Authorization: Bearer <key>, from your server. There is no browser CORS access. An active key sent in a URL is revoked and we email its owner.

An admin key manages models and calculates. A calculate-only key calls calculate and scenarios; use it in production apps. Create and revoke keys in the dashboard.

Use defined workbook names in ref where possible, so new versions survive moved cells. For example, use a name such as Principal instead of Inputs!B2.

Each call starts from the version's saved inputs. Omitted inputs keep their saved value. Outputs are scalars for cells, arrays of rows for ranges, and null for blanks; spreadsheet errors are objects such as {"error":"#N/A"}. Dates are workbook serial numbers.

Only successful calculations and batch rows with outputs count as scenarios. A retry counts again. Usage is committed when the platform has read the whole response body; your connection can still fail after that. Set your client timeout to at least 90 seconds. See all limits.

Within /v1, fields and codes may be added. Ignore unknown fields and handle unknown error codes by HTTP status.

POST /v1/models

Upload a workbook as a new model. Admin key.

Request, response and error codes

post:
      tags: [models]
      operationId: uploadModel
      summary: Upload a workbook as a new model
      description: |
        Creates a model from an `.xlsx` or `.xlsm` file and returns its first
        version and compatibility report.
        - Files up to 10 MiB. The 8 MiB JSON body limit does not apply here.
        - The entire multipart body must be read within 60 s. A slow body
          holds an isolate memory reservation; a 10 MiB upload needs about
          1.4 Mbit/s. Exceeding this deadline answers 503
          `service_unavailable` with `Retry-After` and is not counted.
        - Isolate memory capacity refusals are 503 `service_unavailable`
          with `Retry-After` and are not counted toward daily uploads.
        - The check runs on the server and takes up to 30 s.
        - A model is admitted only if its measured calculation memory is at
          most 98 MiB.
        - One upload at a time per account. It uses one of the plan's
          concurrent slots.
        - Occupies one model slot while the model exists.
        - Counts against daily uploads as listed in "Order of checks".
        - When every check instance is busy, the answer is 503 with
          `Retry-After`; there is no queue.
      security:
        - adminKey: []
      requestBody:
        required: true
        x-max-bytes: 10551296
        x-violation: file_too_large
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  contentMediaType: application/octet-stream
                  description: The workbook. The part's filename becomes `file_name`. Format is recognised from the bytes, not the name.
                  x-max-bytes: 10485760
                  x-violation: file_too_large
      responses:
        '201':
          description: Model created; the first version is active.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/UploadResult'}
        '400': {$ref: '#/components/responses/Error400'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '413': {$ref: '#/components/responses/Error413'}
        '422': {$ref: '#/components/responses/Error422'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [invalid_request, unauthorized, plan_required, model_limit, insufficient_scope, file_too_large, not_xlsx, old_xls, encrypted, damaged_zip, model_too_large, check_timeout, engine_load, rate_limited, upload_limit, concurrency_limited, compute_limit, internal_error, trials_paused, service_unavailable]

GET /v1/models/{model_id}

Read a model, its active version and its interface. Admin key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    get:
      tags: [models]
      operationId: getModel
      summary: Read a model, its active version and its interface
      security:
        - adminKey: []
      responses:
        '200':
          description: The model.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/WithRequestId'
                  - $ref: '#/components/schemas/Model'
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '404': {$ref: '#/components/responses/Error404'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [unauthorized, plan_required, insufficient_scope, not_found, rate_limited, internal_error, trials_paused, service_unavailable]

DELETE /v1/models/{model_id}

Delete a model with all its versions. Admin key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    delete:
      tags: [models]
      operationId: deleteModel
      summary: Delete a model with all its versions
      description: |
        Deletes the model's version files, snapshots, interface and reports from
        primary storage and frees its slot. Usage already counted in the period
        stays.
        Erasure:
        - From this response on, the model answers 404 and nothing can read
          its files. A calculation already running on it finishes; a
          running upload of a new version cannot publish.
        - The deletion task erases the files that exist at deletion:
          usually before this response, otherwise once running
          calculations end, within about 3 minutes.
        - A write that was still in flight can still complete in storage
          later. That file is never readable. The first completed orphan
          sweep cycle that starts after the file is one hour old erases it:
          within 2 days of its appearing (a cycle starts daily and a cycle
          not finished within 12 hours raises an operator alert).
        - Each bound counts per object from its appearance: a late file
          within 2 days; a backup copy within 30 days of being made. There
          is no fixed deadline from deletion by which all content is gone
          while an unknown in-flight write can still arrive.
      security:
        - adminKey: []
      responses:
        '200':
          description: Deleted.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Deleted'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '404': {$ref: '#/components/responses/Error404'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [unauthorized, plan_required, insufficient_scope, not_found, rate_limited, internal_error, trials_paused, service_unavailable]

POST /v1/models/{model_id}/versions

Upload a new version of the workbook under the same model id. Admin key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    post:
      tags: [models]
      operationId: uploadVersion
      summary: Upload a new version of the workbook under the same model id
      description: |
        Same file checks, limits and daily counting as an upload.
        If the model has an interface, the new workbook must keep it. Every
        declared input must still be one constant cell of the declared type,
        and every declared output must resolve.
        The new version becomes active atomically. Calculations already
        running finish on the previous version. A rejected version leaves the
        active one unchanged. Uses no extra slot.
      security:
        - adminKey: []
      requestBody:
        required: true
        x-max-bytes: 10551296
        x-violation: file_too_large
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  contentMediaType: application/octet-stream
                  x-max-bytes: 10485760
                  x-violation: file_too_large
      responses:
        '201':
          description: New version accepted and active.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/VersionResult'}
        '400': {$ref: '#/components/responses/Error400'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '404': {$ref: '#/components/responses/Error404'}
        '409': {$ref: '#/components/responses/Error409'}
        '413': {$ref: '#/components/responses/Error413'}
        '422': {$ref: '#/components/responses/Error422'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [invalid_request, unauthorized, plan_required, insufficient_scope, not_found, version_conflict, file_too_large, not_xlsx, old_xls, encrypted, damaged_zip, model_too_large, check_timeout, engine_load, interface_broken, rate_limited, upload_limit, concurrency_limited, compute_limit, internal_error, trials_paused, service_unavailable]

PUT /v1/models/{model_id}/interface

Declare the model's inputs and outputs. Admin key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    put:
      tags: [models]
      operationId: putInterface
      summary: Declare the model's inputs and outputs
      description: |
        Replaces the whole interface. It is checked against the active version.
        A rejected interface leaves the previous one in force.
        Uses one concurrent slot through an `interface` lease, reserves no
        scenarios, and holds 55 s against the compute limit until measured
        work replaces the hold. Admission can answer 429
        `concurrency_limited` or `compute_limit`.
      security:
        - adminKey: []
      requestBody:
        required: true
        x-max-bytes: 8388608
        x-violation: request_too_large
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Interface'}
            examples:
              quickstart:
                value:
                  inputs:
                    - {name: principal, ref: "Inputs!B2", type: number, min: 1000, max: 10000000}
                    - {name: rate, ref: "Inputs!B3", type: number, min: 0, max: 1}
                    - {name: term_months, ref: "Inputs!B4", type: number, allowed: [120, 180, 240, 360]}
                  outputs:
                    - {name: monthly_payment, ref: "Summary!B2"}
      responses:
        '200':
          description: Interface saved.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InterfaceResult'}
        '400': {$ref: '#/components/responses/Error400'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '404': {$ref: '#/components/responses/Error404'}
        '409': {$ref: '#/components/responses/Error409'}
        '413': {$ref: '#/components/responses/Error413'}
        '422': {$ref: '#/components/responses/Error422'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [invalid_request, unauthorized, plan_required, insufficient_scope, not_found, version_conflict, request_too_large, invalid_interface, rate_limited, concurrency_limited, compute_limit, internal_error, trials_paused, service_unavailable]

GET /v1/models/{model_id}/compatibility

Compatibility report of the active version on the current engine build. Admin key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    get:
      tags: [models]
      operationId: getCompatibility
      summary: Compatibility report of the active version on the current engine build
      description: |
        Every live version is rebuilt with a new calculation-engine build
        before that build starts serving. `engine_build` names the build that
        calculates this version. If a version's snapshot for that build is
        missing, the answer is 503 `service_unavailable` until it is rebuilt.

        If a new build cannot open the version or needs more than 98 MiB, the
        version keeps calculating on the previous build until
        `engine_cutoff_at`, 7 days after the new build's release. The report
        and `engine_build` are then the previous build's. Builds switch at
        most once per 7 days, so the previous build stays available until
        the cutoff. After the cutoff,
        `calculate` and `scenarios` answer 422 `model_too_large` or
        `engine_load`. This lasts until a new version is accepted or a later
        build opens the workbook.
      security:
        - adminKey: []
      responses:
        '200':
          description: The report.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CompatibilityResult'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '403': {$ref: '#/components/responses/Error403'}
        '404': {$ref: '#/components/responses/Error404'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [unauthorized, plan_required, insufficient_scope, not_found, rate_limited, internal_error, trials_paused, service_unavailable]

POST /v1/models/{model_id}/calculate

Calculate one scenario. Admin or calculate-only key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    post:
      tags: [calculation]
      operationId: calculate
      summary: Calculate one scenario
      description: |
        Sets the given declared inputs, recalculates, and returns declared
        outputs by name.
        - Over 30 s of processor time: 422 `calculation_timeout`.
        - The outputs over 4 MiB, serialized as in this response (under
          their output names): 422 `response_too_large`; nothing is
          billed.
        - Bills 1 scenario on 200, including when an output holds a
          spreadsheet error.
        - Stopped by memory on an admitted model: 503 `service_unavailable`.
      security:
        - adminKey: []
        - calcKey: []
      requestBody:
        required: true
        x-max-bytes: 8388608
        x-violation: request_too_large
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CalculateRequest'}
            examples:
              quickstart:
                value: {inputs: {principal: 250000, rate: 0.045, term_months: 360}}
      responses:
        '200':
          description: Calculated.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CalculateResult'}
              examples:
                quickstart:
                  value:
                    request_id: req_Z3J5c2R1bW15cmVxdWVzdA
                    model_id: mdl_bG9hbm1vZGVsZXhhbXBsZQ
                    version_id: ver_Zmlyc3R2ZXJzaW9uZXhhbQ
                    engine_build: bbb9fe99
                    outputs: {monthly_payment: 1365.0655645475256}
                    warnings: []
                    billed_scenarios: 1
        '400': {$ref: '#/components/responses/Error400'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '404': {$ref: '#/components/responses/Error404'}
        '409': {$ref: '#/components/responses/Error409'}
        '413': {$ref: '#/components/responses/Error413'}
        '422': {$ref: '#/components/responses/Error422'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [invalid_request, unauthorized, plan_required, model_over_plan_limit, quota_exceeded, not_found, interface_required, request_too_large, invalid_input, unknown_output, calculation_timeout, engine_error, response_too_large, model_too_large, engine_load, rate_limited, concurrency_limited, compute_limit, internal_error, trials_paused, service_unavailable]

POST /v1/models/{model_id}/scenarios

Calculate a batch of scenarios. Admin or calculate-only key.

Request, response and error codes

parameters:
      - $ref: '#/components/parameters/ModelId'
    post:
      tags: [calculation]
      operationId: runScenarios
      summary: Calculate a batch of scenarios
      description: |
        Runs up to 10,000 scenarios. The whole batch is reserved against the
        quota first: a batch larger than the remaining quota is refused with 402
        `quota_exceeded`, with no partial run.

        Rows come back in order. Each row has either `outputs` or `error`. A
        scenario with a bad input becomes an error row and the batch continues.

        A batch can stop early with `truncated=true`:
        - `time_limit`: no new part starts after 25 s. A part that runs past
          30 s of processor time is lost, but the rows of earlier parts stay.
        - `response_value_limit`: 100,000 output values.
        - `response_size_limit`: the next row does not fit the 8 MiB of
          serialized response, or a later row's outputs exceed 4 MiB
          serialized under their output names. The rows before it are
          returned.
        - `engine_error`, for one of two reasons:
          - the engine failed on a scenario; that scenario is the last row
            and `completed` includes it;
          - a later part was stopped by memory; the rows of earlier parts are
            returned and `completed` counts only them.

        Whole-batch errors:
        - the first row's outputs over 4 MiB (serialized under their
          output names): 422 `response_too_large`;
        - the first part stopped by memory: 503 `service_unavailable`.

        To resume, send `scenarios.slice(completed)`; indexes restart at 0.
        Only rows with `outputs` are billed. The worst-case response time is
        about 80 s (see "Time").
      security:
        - adminKey: []
        - calcKey: []
      requestBody:
        required: true
        x-max-bytes: 8388608
        x-violation: request_too_large
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ScenariosRequest'}
      responses:
        '200':
          description: Batch calculated, possibly truncated.
          headers:
            X-Request-Id: {$ref: '#/components/headers/X-Request-Id'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ScenariosResult'}
        '400': {$ref: '#/components/responses/Error400'}
        '401': {$ref: '#/components/responses/Error401'}
        '402': {$ref: '#/components/responses/Error402'}
        '404': {$ref: '#/components/responses/Error404'}
        '409': {$ref: '#/components/responses/Error409'}
        '413': {$ref: '#/components/responses/Error413'}
        '422': {$ref: '#/components/responses/Error422'}
        '429': {$ref: '#/components/responses/Error429'}
        '500': {$ref: '#/components/responses/Error500'}
        '503': {$ref: '#/components/responses/Error503'}
      x-error-codes: [invalid_request, unauthorized, plan_required, model_over_plan_limit, quota_exceeded, not_found, interface_required, request_too_large, batch_too_large, unknown_output, response_too_large, model_too_large, engine_load, rate_limited, concurrency_limited, compute_limit, internal_error, trials_paused, service_unavailable]

Errors

Every JSON error has request_id and error.code / error.message. Extra fields are listed in ErrorBody below. Every response carries X-Request-Id; 429 and 503 also carry Retry-After in seconds. Error responses bill no scenarios.

Public API error codes
HTTP statusCode
400invalid_request
401unauthorized
402model_limit
402model_over_plan_limit
402plan_required
402quota_exceeded
403insufficient_scope
404not_found
409interface_required
409version_conflict
413batch_too_large
413file_too_large
413request_too_large
422calculation_timeout
422check_timeout
422damaged_zip
422encrypted
422engine_error
422engine_load
422interface_broken
422invalid_input
422invalid_interface
422model_too_large
422not_xlsx
422old_xls
422response_too_large
422unknown_output
429compute_limit
429concurrency_limited
429rate_limited
429upload_limit
500internal_error
503service_unavailable
503trials_paused

Fields and schemas

The definitions below are linked from each operation. Required fields, types, constraints and response fields come from the API contract.

Retry-After

Retry-After:
      description: Seconds until a retry can succeed.
      schema: {type: integer, minimum: 1}

X-Request-Id

X-Request-Id:
      description: Unique per request; equals `request_id` in /v1 bodies.
      schema: {$ref: '#/components/schemas/RequestId'}

ModelId

ModelId:
      name: model_id
      in: path
      required: true
      description: '`mdl_` + 22 base64url chars. Unknown, malformed and other accounts'' ids all give 404 `not_found`.'
      schema: {type: string, examples: [mdl_bG9hbm1vZGVsZXhhbXBsZQ]}

ApiName

ApiName:
      type: string
      pattern: '^[A-Za-z_][A-Za-z0-9_.-]{0,63}$'
      description: Interface name. Cell addresses and workbook names are never accepted in place of it.

BilledScenarios

BilledScenarios:
      type: integer
      minimum: 0
      description: |
        Scenarios this response bills: 1 for calculate, the rows with
        `outputs` for scenarios. Usage is committed after the platform has
        read the whole body (info "Billing"). A body cut off before that is
        not billed, and a failure after it can leave the response unbilled.
        It is never billed twice.

CalculateRequest

CalculateRequest:
      type: object
      additionalProperties: false
      properties:
        inputs: {$ref: '#/components/schemas/InputMap'}
        outputs: {$ref: '#/components/schemas/OutputSelection'}

CalculateResult

CalculateResult:
      allOf:
        - $ref: '#/components/schemas/WithRequestId'
        - type: object
          required: [model_id, version_id, engine_build, outputs, warnings, billed_scenarios]
          properties:
            model_id: {type: string}
            version_id: {type: string, description: The version that calculated (the one active when the request started).}
            engine_build: {$ref: '#/components/schemas/EngineBuild'}
            outputs:
              type: object
              additionalProperties: {$ref: '#/components/schemas/OutputValue'}
            warnings: {$ref: '#/components/schemas/Warnings'}
            billed_scenarios: {$ref: '#/components/schemas/BilledScenarios'}

CellRef

CellRef:
      type: string
      minLength: 1
      maxLength: 255
      description: 'A cell (`Inputs!B2`, `''Loan Inputs''!B2`), a range for outputs (`Summary!B2:B6`), or a defined name, workbook-wide or sheet-scoped (`Principal`, `''Inputs''!Principal`). Defined names survive moved cells in new versions.'

CellValue

CellValue:
      oneOf:
        - {type: number}
        - {type: string}
        - {type: boolean}
        - {type: 'null'}
        - $ref: '#/components/schemas/ErrorValue'

CheckValue

CheckValue:
      type: object
      required: [k]
      description: 'Value in a mismatch: k = n (number), s (string), b (boolean), e (error), empty.'
      properties:
        k: {enum: [n, s, b, e, empty]}
        v: {}

CompatibilityResult

CompatibilityResult:
      allOf:
        - $ref: '#/components/schemas/WithRequestId'
        - type: object
          required: [model_id, version_id, engine_build, engine_cutoff_at, engine_cutoff_reason, report]
          properties:
            model_id: {type: string}
            version_id: {type: string}
            engine_build: {$ref: '#/components/schemas/EngineBuild'}
            engine_cutoff_at: {$ref: '#/components/schemas/EngineCutoff'}
            engine_cutoff_reason: {$ref: '#/components/schemas/EngineCutoffReason'}
            report: {$ref: '#/components/schemas/Report'}

Deleted

Deleted:
      type: object
      required: [model_id, deleted]
      properties:
        request_id: {$ref: '#/components/schemas/RequestId'}
        model_id: {type: string}
        deleted: {const: true}

EngineBuild

EngineBuild:
      type: string
      pattern: '^[0-9a-f]{8,40}$'
      description: Build of the calculation engine that produced a report or a result.

EngineCutoff

EngineCutoff:
      type: [string, 'null']
      format: date-time
      description: 'Set when the current engine build cannot open this version or needs more than 98 MiB. Until this time the version calculates on the previous build; afterwards calculate and scenarios answer 422 model_too_large or engine_load. null otherwise.'

EngineCutoffReason

EngineCutoffReason:
      enum: [model_too_large, engine_load, null]
      description: 'Set with engine_cutoff_at: the 422 code calculate and scenarios answer after the cutoff. null otherwise.'

ErrorBody

ErrorBody:
      type: object
      required: [error]
      properties:
        request_id:
          $ref: '#/components/schemas/RequestId'
          description: Present on every /v1 response.
        error:
          type: object
          required: [code, message]
          properties:
            code: {$ref: '#/components/schemas/ErrorCode'}
            message: {type: string, description: Human-readable; no server paths, stack traces or engine text.}
            billing_url: {type: string, format: uri, const: 'https://app.workbookapi.com/billing', description: Every 402.}
            remaining: {type: integer, minimum: 0, description: 'quota_exceeded: max(0, quota − used − reserved), where quota is the admission quota — the lowest of the period quota and the quota of each plan change still pending at Paddle.'}
            resets_at: {type: string, format: date-time, description: 'quota_exceeded, compute_limit, usage_above_target_quota: end of the current usage period (for a trial: the trial end, after which access ends); upload_limit: next 00:00 UTC.'}
            used: {type: integer, minimum: 0, description: 'usage_above_target_quota: scenarios of this period, billed plus reserved by calls in progress.'}
            quota: {type: integer, minimum: 0, description: 'usage_above_target_quota: the refusal bound — the lower of the requested plan''s quota and this period''s quota after the change, computed for the moment of the request.'}
            required_mib: {type: [number, 'null'], description: 'model_too_large: measured calculation memory; null when the workbook was refused before it could be measured (pre-screen estimate, workbook text over the string limit).'}
            limit_mib: {const: 98, description: model_too_large.}
            limit_bytes: {type: integer, description: 'file_too_large: 10485760; request_too_large: 8388608; response_too_large: 4194304.'}
            limit: {type: integer, description: 'batch_too_large: 10000.'}
            details:
              type: array
              description: invalid_request, invalid_interface, interface_broken, invalid_input, unknown_output.
              items: {$ref: '#/components/schemas/Problem'}
            report:
              $ref: '#/components/schemas/Report'
              description: engine_load on upload and new version.

ErrorCode

ErrorCode:
      type: string
      enum:
        - batch_too_large
        - calculation_timeout
        - check_timeout
        - compute_limit
        - concurrency_limited
        - damaged_zip
        - encrypted
        - engine_error
        - engine_load
        - file_too_large
        - insufficient_scope
        - interface_broken
        - interface_required
        - internal_error
        - invalid_input
        - invalid_interface
        - invalid_request
        - model_limit
        - model_over_plan_limit
        - model_too_large
        - not_found
        - not_xlsx
        - old_xls
        - plan_required
        - quota_exceeded
        - rate_limited
        - request_too_large
        - response_too_large
        - service_unavailable
        - trials_paused
        - unauthorized
        - unknown_output
        - upload_limit
        - version_conflict

ErrorValue

ErrorValue:
      type: object
      required: [error]
      additionalProperties: false
      properties:
        error: {type: string, examples: ['#N/A', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!', '#NULL!', '#SPILL!', '#CALC!']}

InputMap

InputMap:
      type: object
      additionalProperties: {$ref: '#/components/schemas/Scalar'}
      description: Declared input name → value. Undeclared name, wrong type, outside min/max or not in allowed → `invalid_input`.

Interface

Interface:
      type: object
      required: [inputs, outputs]
      additionalProperties: false
      description: Names are unique across inputs and outputs; two inputs may not share a cell. At most 4,096 inputs, 100 outputs and 10,000 output cells per scenario — over a limit is 422 `invalid_interface`, not 400.
      properties:
        inputs:
          type: array
          maxItems: 4096
          x-violation: invalid_interface
          items: {$ref: '#/components/schemas/InterfaceInput'}
        outputs:
          type: array
          minItems: 1
          maxItems: 100
          x-violation: invalid_interface
          items: {$ref: '#/components/schemas/InterfaceOutput'}
      x-limits:
        output_cells_per_scenario: 10000

InterfaceInput

InterfaceInput:
      type: object
      required: [name, ref, type]
      additionalProperties: false
      description: |
        The ref must be one constant or blank cell, or a name for one such cell.
        The declared type must match a non-blank cell's value.
        `min`/`max` apply to `number` only and cannot be combined with
        `allowed`; `allowed` values must be of the declared type.
        Breaking a rule gives 422 `invalid_interface`.
      properties:
        name: {$ref: '#/components/schemas/ApiName'}
        ref: {$ref: '#/components/schemas/CellRef'}
        type: {enum: [number, string, boolean]}
        min: {type: number}
        max: {type: number}
        allowed:
          type: array
          minItems: 1
          items: {$ref: '#/components/schemas/Scalar'}

InterfaceOutput

InterfaceOutput:
      type: object
      required: [name, ref]
      additionalProperties: false
      properties:
        name: {$ref: '#/components/schemas/ApiName'}
        ref: {$ref: '#/components/schemas/CellRef'}

InterfaceResult

InterfaceResult:
      allOf:
        - $ref: '#/components/schemas/WithRequestId'
        - type: object
          required: [model_id, version_id, interface]
          properties:
            model_id: {type: string}
            version_id: {type: string, description: The active version the interface was checked against.}
            interface: {$ref: '#/components/schemas/Interface'}

Model

Model:
      type: object
      required: [model_id, file_name, created_at, state, active_version, interface]
      properties:
        model_id: {type: string}
        file_name: {type: string, maxLength: 255, description: File name of the active version.}
        created_at: {type: string, format: date-time}
        state: {$ref: '#/components/schemas/ModelState'}
        active_version:
          type: object
          required: [version_id, created_at, engine_build, engine_cutoff_at, engine_cutoff_reason]
          properties:
            version_id: {type: string}
            created_at: {type: string, format: date-time}
            engine_build: {$ref: '#/components/schemas/EngineBuild'}
            engine_cutoff_at: {$ref: '#/components/schemas/EngineCutoff'}
            engine_cutoff_reason: {$ref: '#/components/schemas/EngineCutoffReason'}
        interface:
          oneOf:
            - $ref: '#/components/schemas/Interface'
            - {type: 'null'}

ModelState

ModelState:
      type: string
      enum: [callable, no_interface, over_plan_limit, plan_required, past_engine_cutoff]
      description: |
        The first failing check of the /v1 order, as calculate would meet it:
        - plan_required: dashboard only, the account has no access (402);
        - over_plan_limit: 402 model_over_plan_limit;
        - no_interface: 409 interface_required;
        - past_engine_cutoff: the active version is past engine_cutoff_at,
          so calculate answers 422 with engine_cutoff_reason;
        - callable: none of these.
        A version before its cutoff stays callable; engine_cutoff_at shows
        the date.

OutputSelection

OutputSelection:
      type: array
      minItems: 1
      maxItems: 100
      uniqueItems: true
      items: {type: string}
      description: Declared output names; default all declared outputs. An undeclared name or a cell address is 422 `unknown_output`.

OutputValue

OutputValue:
      description: A scalar for a single cell; an array of rows for a range, a multi-cell name or a dynamic array.
      oneOf:
        - $ref: '#/components/schemas/CellValue'
        - type: array
          items:
            type: array
            items: {$ref: '#/components/schemas/CellValue'}

Problem

Problem:
      type: object
      required: [problem]
      properties:
        element: {enum: [input, output]}
        name: {type: string, description: API name as sent by the client.}
        field: {type: string, description: 'invalid_request: JSON pointer of the offending field.'}
        problem:
          type: string
          enum:

            - invalid_json
            - schema
            - content_type

            - not_declared
            - type
            - out_of_range
            - not_allowed

            - formula
            - multi_cell
            - type_mismatch
            - not_found
            - duplicate_name
            - duplicate_cell
            - invalid_bounds
            - too_many_inputs
            - too_many_outputs
            - too_many_cells
        expected: {type: string, description: 'type / type_mismatch: the declared type.'}

Report

Report:
      type: object
      required: [ok, engine_build]
      description: |
        Compatibility check, same function as the browser check on the site.
        The site's camelCase keys map 1:1 to the snake_case keys here, for
        example `matchedStrict` → `matched_strict` and `frozenUdf` →
        `frozen_udf`. Feature keys map the same way.
        With `ok=false` the check was incomplete (`reason`). The model is still
        accepted, and the counts may be missing.
      properties:
        ok: {type: boolean}
        reason:
          enum: [not_saved_by_excel, no_checkable_formula_cells, no_results_saved_by_excel, engine_load]
          description: Only when ok=false.
        message: {type: string, description: Explanation of `reason`, as on the site. Never contains engine text.}
        cells: {type: integer, minimum: 0, description: Formula cells compared.}
        matched: {type: integer, minimum: 0, description: Relative tolerance 1e-6.}
        matched_strict: {type: integer, minimum: 0, description: Relative tolerance 1e-9.}
        mismatches:
          type: array
          maxItems: 200
          items:
            type: object
            required: [cell, formula, expected, got, cause]
            properties:
              cell: {type: string, examples: ['Summary!B7']}
              formula: {type: string}
              expected: {$ref: '#/components/schemas/CheckValue'}
              got: {$ref: '#/components/schemas/CheckValue'}
              cause: {enum: [named_formula, unsupported_function, table_reference, other]}
        mismatches_truncated: {type: boolean}
        features:
          type: object
          additionalProperties: false
          properties:
            named_formulas: {type: boolean}
            vba: {type: boolean}
            external_links: {type: boolean}
            external_data: {type: boolean, description: External data or Power Query connections.}
            pivot_tables: {type: boolean}
            strict_ooxml: {type: boolean}
            iterative_calc: {type: boolean}
            date1904: {type: boolean}
            data_tables: {type: boolean}
        excluded:
          type: object
          description: Skipped formula cells by reason (for example volatile, no_cached_value, external_ref, data_table, frozen_udf).
          additionalProperties: {type: integer, minimum: 1}
        frozen_udf:
          type: object
          required: [count, functions]
          properties:
            count: {type: integer, minimum: 0}
            functions: {type: array, items: {type: string}}
        calc_mode: {enum: [auto, manual, autoNoTable]}
        date_system: {enum: ['1900', '1904']}
        load_ms: {type: number}
        eval_ms: {type: number}
        engine_build: {$ref: '#/components/schemas/EngineBuild'}

RequestId

RequestId:
      type: string
      pattern: '^req_[A-Za-z0-9_-]{22}$'

RowError

RowError:
      type: object
      required: [code, message]
      properties:
        code: {enum: [invalid_input, engine_error]}
        message: {type: string}
        details:
          type: array
          items: {$ref: '#/components/schemas/Problem'}

Scalar

Scalar:
      oneOf:
        - {type: number}
        - {type: string}
        - {type: boolean}

ScenarioRow

ScenarioRow:
      oneOf:
        - type: object
          required: [index, outputs]
          additionalProperties: false
          properties:
            index: {type: integer, minimum: 0}
            outputs:
              type: object
              additionalProperties: {$ref: '#/components/schemas/OutputValue'}
        - type: object
          required: [index, error]
          additionalProperties: false
          properties:
            index: {type: integer, minimum: 0}
            error: {$ref: '#/components/schemas/RowError'}

ScenariosRequest

ScenariosRequest:
      type: object
      required: [scenarios]
      additionalProperties: false
      properties:
        scenarios:
          type: array
          minItems: 1
          maxItems: 10000
          x-violation: batch_too_large
          items:
            type: object
            additionalProperties: false
            properties:
              inputs: {$ref: '#/components/schemas/InputMap'}
        outputs: {$ref: '#/components/schemas/OutputSelection'}

ScenariosResult

ScenariosResult:
      allOf:
        - $ref: '#/components/schemas/WithRequestId'
        - type: object
          required: [model_id, version_id, engine_build, rows, requested, completed, truncated, truncation_reason, billed_scenarios, warnings]
          properties:
            model_id: {type: string}
            version_id: {type: string}
            engine_build: {$ref: '#/components/schemas/EngineBuild'}
            rows:
              type: array
              maxItems: 10000
              items: {$ref: '#/components/schemas/ScenarioRow'}
            requested: {type: integer, minimum: 1, maximum: 10000}
            completed: {type: integer, minimum: 0, description: Rows returned, error rows included. Resume with scenarios.slice(completed).}
            truncated: {type: boolean}
            truncation_reason:
              enum: [time_limit, response_value_limit, response_size_limit, engine_error, null]
            billed_scenarios: {$ref: '#/components/schemas/BilledScenarios'}
            warnings: {$ref: '#/components/schemas/Warnings'}

UploadResult

UploadResult:
      allOf:
        - $ref: '#/components/schemas/WithRequestId'
        - type: object
          required: [model_id, version_id, file_name, report]
          properties:
            model_id: {type: string}
            version_id: {type: string}
            file_name: {type: string}
            report: {$ref: '#/components/schemas/Report'}

VersionResult

VersionResult:
      allOf:
        - $ref: '#/components/schemas/UploadResult'
        - type: object
          required: [previous_version_id, changed_inputs]
          properties:
            previous_version_id: {type: string}
            changed_inputs:
              type: [array, 'null']
              items: {$ref: '#/components/schemas/ApiName'}
              description: Declared inputs whose saved value differs from the previous version. Empty without an interface. null when the comparison could not run.

Warnings

Warnings:
      type: array
      items: {type: string}
      description: Workbook-level warnings (mismatch count, unsupported features, skipped cells, manual mode) and add-in/macro reach naming affected outputs by API name. Never sheet names, cell addresses, formulas or workbook function names.

WithRequestId

WithRequestId:
      type: object
      required: [request_id]
      properties:
        request_id: {$ref: '#/components/schemas/RequestId'}