---
title: "Scope a property turnover (rendered as xlsx)"
method: POST
path: "/v1/property-turnover/scope/render/xlsx"
tags: ["property-turnover"]
---

# Scope a property turnover (rendered as xlsx)

`POST /v1/property-turnover/scope/render/xlsx`

## Request

- Base URL: `https://tektolo.com` (the document declares no server; this is the origin it was published from)
- URL: `https://tektolo.com/v1/property-turnover/scope/render/xlsx`
- Auth: none declared

Same body as the operation. Runs it, then renders the result as application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. The artifact itself is not stored. Rate limited more tightly than the JSON endpoint.

## Headers

- `Tektolo-Ruleset` string, nullable: Optional. The ruleset version you built against, as results carry it in provenance.ruleset_version (e.g. 'sd-2026.11.14'), or its sha256: hash. If the rules have changed since, the call fails (nothing computed, nothing charged) and says what is in force now, so you upgrade on your own schedule. Leave it out to answer under the current rules.
- `Tektolo-Ruleset-Mode` string, nullable: Optional, with Tektolo-Ruleset. 'serve' asks us to answer under the ruleset version you named even when the rules have changed since, from that version's signed bundle. The answer says so (run.served_from_bundle and the Tektolo-Ruleset-Served header). We refuse (412, with a reason) a version older than 12 months, one a later correction or law change replaced for your state, or one we have no bundle for. Leave it out to be refused whenever the rules have changed.

## Request body

- ScopeRequest
  - `client_refs` object, nullable: Your own reference ids (e.g. unit_id, job_id). Echoed back, never used.
  - `property` PropertyProfile, required
    - `zip` string, required: 5-digit US ZIP code (no street address).
    - `unit_type` 'apartment' | 'single_family' | 'townhome' | 'condo' | 'duplex' | 'manufactured'
    - `bedrooms` integer, required: 0 for a studio.
    - `bathrooms` union, required: Half baths count as 0.5.
      - number
      - string
    - `sq_ft` integer, nullable: Interior sq ft.
    - `year_built` integer, nullable: Drives lead-paint (pre-1978) handling.
    - `floor_coverings` FloorCoverings: What is on the floors. Drives the carpet-cleaning baseline and whole-unit carpet sizes.
      - `carpet_present` boolean, nullable: Is there wall-to-wall carpet? Omit if unknown: carpet findings then imply carpet; with no carpet findings, whole-unit carpet cleaning is left out and listed as unknown.
      - `carpet_sq_ft` integer, nullable: Carpeted floor area. Omit to assume 60% of the unit floor area.
  - `findings` Finding[]: Move-out inspection findings, one per condition, with location and extent.
    - `id` string, nullable: Your id for this finding; defaults to f1, f2, ... in order.
    - `location` Location: Where the finding is. Also accepted as a plain string ("Bedroom 2", "kitchen").
      - `area` 'whole_unit' | 'kitchen' | 'living_room' | 'dining_room' | 'bedroom' | 'bathroom' | 'hallway' | 'entry' | 'laundry' | 'closet' | 'garage' | 'patio_balcony' | 'exterior' | 'yard' | 'other'
      - `label` string, nullable: e.g. 'Bedroom 2'.
    - `code` string, nullable: Optional. Only a code returned by property_turnover_issue_codes (e.g. 'walls.nail_holes'); otherwise leave it out and use description.
    - `description` string, nullable: Plain description if no code, e.g. 'dog urine stains in bedroom carpet'.
    - `extent` 'spot' | 'partial' | 'full'
    - `quantity` Quantity
      - `value` union, required
        - number
        - string
      - `uom` 'each' | 'sq_ft' | 'sq_ft_floor' | 'sq_yd' | 'linear_ft' | 'room' | 'unit' | 'bedroom' | 'bathroom' | 'hour' | 'cu_yd' | 'visit', required
    - `notes` string, nullable
    - `extraction` Extraction: How a finding was produced, when software read it from free text (contract 0.4.0) or from an uploaded document (``ai_document`` and ``page``, additive). Set by the web tool when its AI reader (Azure OpenAI) turned a person's own words, or a page of their inspection report, into this finding. The engine reports such a finding as ``resolution: "ai"`` with ``needs_review`` and flags its lines; it never changes how the finding is priced.
      - `method` 'ai' | 'ai_document', required: 'ai': read by a language model from free text. 'ai_document': read by a language model from a page of an uploaded document.
      - `model` string, required: The model that read it, e.g. 'gpt-5-nano 2025-08-07'.
      - `confidence` union, required: The reader's confidence, 0 to 1.
        - number
        - string
      - `page` integer, nullable: 'ai_document' only: the page of the upload it was read from (1-based).
      - `quote` string, nullable: 'ai_document' only: the row's words as printed on that page, contact details masked. Evidence for checking the finding; it never changes the price.
      - `review` ExtractionReview: What the person did with a finding the web tool read from an uploaded document, before estimating (contract 0.11.0, additive). ``confirmed``: they pressed "Looks right"; ``accepted``: left as read, on a row the page did not flag; ``edited``: they changed it (``changed`` names what). A reviewed finding keeps its ``extraction`` (where it came from) but is no longer reported as needing review: the person has checked it.
        - `outcome` 'confirmed' | 'accepted' | 'edited', required
        - `flags` string[]: Why the page flagged it, if it did.
        - `changed` string[]: 'edited': the fields the person changed.
    - `confirmed` boolean: true: the person checked this AI-read finding (one with extraction) and it says what they meant. It keeps resolution 'ai' but no longer needs review, and the answer is verified once every AI-read finding is confirmed.
    - `signature_like` boolean: Set by Tektolo's notes reader (reading.findings, the web tool's rows): the words came right after a sign-off and read like a name or signature ('Thanks!' / 'Rotten Egg Smell'), kept only because they name a safety item. A safety item in them is raised unconfirmed (check first). Keep it when you send such a finding back, so every way of sending the same words gives the same answer.
  - `standards` Standards
    - `profile` 'minimum' | 'standard' | 'premium'
    - `overrides` StandardsOverrides
      - `paint_policy` 'touch_up' | 'full_if_needed' | 'always_full', nullable
      - `carpet_policy` 'clean' | 'replace_if_damaged' | 'always_replace', nullable
      - `cleaning_policy` 'standard' | 'deep', nullable
      - `always_rekey` boolean, nullable
      - `markup_pct` union: Management markup applied to every line, %.
        - number
        - string
    - `price_tier` 'bulk_contract' | 'pm_vendor' | 'retail_one_off': Purchasing channel. Library prices are pm_vendor (a PM buying turns from a local vendor).
  - `rate_card` RateCardEntry[]: Your own prices by trade or task; they replace the regional library prices.
    - `task_id` string, nullable: A task_id from a result line, e.g. 'carpet.clean'.
    - `trade` 'hauling' | 'handyman' | 'drywall' | 'painter' | 'flooring' | 'carpet_cleaning' | 'cleaning' | 'appliance' | 'plumber' | 'electrician' | 'hvac' | 'locksmith' | 'pest_control' | 'landscaping' | 'window_glass' | 'environmental' | 'structural'
    - `unit_price` union: USD per uom. On a trade entry, uom 'hour' is an hourly labor rate (replaces the labor part of that trade's prices); another uom prices that trade's tasks in it.
      - number
      - string
    - `uom` 'each' | 'sq_ft' | 'sq_ft_floor' | 'sq_yd' | 'linear_ft' | 'room' | 'unit' | 'bedroom' | 'bathroom' | 'hour' | 'cu_yd' | 'visit'
    - `min_charge` union: USD per trade visit.
      - number
      - string
    - `trip_charge` union: USD per trade visit.
      - number
      - string
  - `trade_vendors` object: Vendor (name, contact) to put on each trade's lines.
  - `constraints` Constraints
    - `target_ready_date` string, date, nullable
    - `occupied` boolean: Work happens while occupied.
    - `access_notes` string, nullable
  - `tax_rate` union: Applied to all lines, e.g. 0.07.
    - number
    - string
  - `decisions` Decision[]: Approve, decline or defer lines or findings from an earlier scope.
    - `line_id` string, nullable
    - `finding_id` string, nullable
    - `action` 'approve' | 'decline' | 'defer', required
    - `note` string, nullable
  - `read` 'parser' | 'ai': parser (default): descriptions are read by the deterministic parser and matcher only; what they cannot place comes back as a manual-quote line with suggested codes. ai: what the parser cannot place is then read by an AI model; the answer is a draft (review.status) and echoes the reading, whose findings you confirm and send back for a verified answer (contract 0.12.0).
  - `retain` boolean: Optional (contract 0.5.0). false: keep none of this request's text. By default the finding descriptions are kept with contact details and access codes removed, to improve matching; the estimate is the same either way.
  - `detail` 'summary' | 'full': summary: totals, trade scopes, every line tied to a finding (capped at 40) plus the largest must-fix/refer-out lines, a one-line note on the routine lines left out, notes, and only findings needing attention (small). full: every line, finding and source.

## Response `200`

Successful Response

## Other responses

- `412`: The ruleset named in the Tektolo-Ruleset header is not the one in force, and the call did not ask to be answered under it (Tektolo-Ruleset-Mode: serve) or that version can't be served (the body's reason says why). Nothing was computed or charged; the body names both versions.
- `422`: Validation Error

---

[API](https://skmtc.dev/tektolo/apis/tektolo-capabilities-api.md) · [All operations](https://skmtc.dev/tektolo/apis/tektolo-capabilities-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tektolo/apis/tektolo-capabilities-api/revisions/6e667aa7b6f7?raw)
