Skip to main content
Breaking changes to result schemas ship as a new schema_version with advance notice and an overlap window — your integration always knows which shape it’s reading, because every result declares its version.

2026-11

  • Read and change finished underwritings from your own assistant. New MCP tools and endpoints let your AI read a report piece by piece (get_underwriting_outline, get_underwriting_section), list its assumptions, preview a what-if, and — with your user’s confirmation — start a new version with changed assumptions. Underwritings now carry version and previous_underwriting_id. Send X-Cactus-End-User to limit a connection to one of your users. See MCP Server.

2026-10

  • Sage and the full workspace in private embeds. An embedded report now opens the full report workspace: your customer can download the Excel model, ask Sage about the deal, and approve assumption changes that create a new version. Choose a light, dark, or system theme with the theme URL parameter or a cactus:set-theme message. See Private embed.
  • A real example underwriting in sandbox. Register documents as example-underwriting-rent-roll.xlsx and example-underwriting-t12.xlsx and a sandbox underwriting completes instantly with a real report link, Excel model, and results. See Sandbox.
  • Simpler underwriting delivery. notification_email is now optional for public-link delivery; omit it to receive report.url without an email. An opened embedded report now lasts 12 hours with no renewal, so your backend only creates the launch URL. The embed-session renew and revoke endpoints are removed. To cut off access, disable the API key or ask Cactus to remove the parent origin.
  • Private underwriting embeds. Request a single-use iframe launch URL with POST /v1/underwritings/{id}/embed-sessions, supplying the owning end_user_ref and a registered parent_origin. Launch within 60 seconds; the report-only access lease lasts 15 minutes, renews through the authenticated host, and can be revoked by DELETE. POST /v1/underwritings/{id}/embed-sessions/{session_id}/renew keeps an authorized viewer connected without reloading the iframe. Start runs with options.delivery: "private" to avoid public share links and completion emails. Private delivery requires end_user_ref and does not require notification_email; the default public_link behavior remains available. See private embedding.
  • Underwriting setup matches the available app controls. Send options.perspective: "broker" and optional options.notes (up to 10,000 characters), alongside options.mode and role-assigned documents. Notes reach the same Analyst input as the app’s Notes field. Broker is the only available perspective; Investor and return targets are not exposed. See the underwriting walkthrough.

2026-09

  • The completion email now arrives. notification_email on POST /v1/underwritings has always been documented as receiving the report link, and since the pipeline change below it did not. A completed live run now emails that address a link to the same public report as report.url. Sandbox runs still send nothing.
  • Underwriting now returns the whole report, not just a link. A completed GET /v1/underwritings/{id} carries three things in one object: report.url, a public link anyone can open with no login; report.excel_url, pointing at the new GET /v1/underwritings/{id}/model for the populated Excel workbook; and results, the full report as structured data (broker-public-underwriting-delivery-v1) — property, eight chapters covering valuation, income, revenue, costs, comparables, buyer returns and market risk, the ranked critical assumptions, and the readiness disclosures. A flat summary lifts the headline metrics out of it. Every figure is a {state, value, reason} triple, so a value the run could not establish says so instead of arriving as a zero. results is returned only when you fetch a single run; list responses and webhook payloads omit it. Behind that, the endpoint now runs the same underwriting pipeline the Cactus app runs, which changes the request: documents is required and replaces document_ids, because a run reads the property’s own documents and needs to know what each one is. Send [{"document_id": "doc_…", "role": "rent_roll"}]; omit role and it is taken from the document’s declared_type. At most one document per role (rent_roll, t12, financial_model, offering_memorandum, supporting), and each role accepts particular formats — an offering memorandum must be a PDF, a financial model a spreadsheet. Mismatches are rejected at acceptance, unbilled, rather than failing a run half an hour in. options.investment_strategy is replaced by options.mode (light / medium / heavy), and progress.stage now names the pipeline step behind the coarse status.
  • /v1/underwriting-inputs is removed. It returned the underwriting model’s populated input fields, which is what results now gives you as part of a finished underwriting — with the analysis and the workbook alongside it, rather than a field set on its own. The underwriting_inputs.completed and underwriting_inputs.failed events are gone with it. No account had it in production.

2026-08

  • Extractions are more accurate and take longer; the documented durations now match. We moved rent-roll extraction to a stronger model after finding it was misreading columns — square footage landing in market rent, market rent landing in rent, unit status dropped entirely. On the documents where we could measure the same file under both models, that cost 1.4x to 5.5x in run time. The published figures had said “1-5 minutes” for everything, which was no longer true for rent rolls. Measured over 79 live extractions: a T-12, offering memorandum or other document usually finishes in 2-5 minutes (91% within five); a rent roll runs a second consolidation phase and is the slow path, with most within 10 minutes and a large one reaching 30. The 60-minute ceiling is unchanged, and no refund, polling or webhook behaviour changed. If your integration hard-codes a timeout under ten minutes, raise it — poll_after_seconds and the wait long-poll are there so you never have to guess.
  • asset_class is now required on POST /v1/extractions. It was optional, which read as a hint but behaved as a cliff. Without a class, rent-roll consolidation fell back to the cross-class column subset — 14 standard columns instead of multifamily’s 29 — so market_rent, area, bedrooms, bathrooms, bed_bath, move_in, move_out, deposit, concession, other_income, floor, furnished, escalation, subsidized and subsidy_amount stopped being standard fields and came back under custom__* keys named after whatever the source document’s header happened to say. Two rent rolls of the same asset class could therefore disagree — custom__sqft in one, custom__sq_feet in the next — and nothing in the response explained why. Requests without the field now return 400 invalid_request naming asset_class, rather than quietly returning the narrower shape. Accepted values are unchanged: multifamily, self-storage, industrial-outdoor-storage, commercial, industrial, office, retail, hotel. The asset_class argument on the create_extraction MCP tool is required to match. Adding the field is the whole migration; callers already sending it are unaffected.

2026-07

  • Per-product account access, and an errors reference. Accounts are now enabled per product, so an endpoint your account hasn’t bought returns 403 with the new product_not_enabled code and a message naming what to do about it, rather than working. The existing missing_scope code now means something narrower: the account has the product, but the key you used was issued without it. Both are documented alongside every other code the API returns on the new Errors page. Two related changes: /v1/underwriting-inputs now belongs to the underwriting product rather than extraction, and the MCP server advertises only the tools your key can use. No change for accounts enabled for everything.
  • T-12 month headers: Spanish abbreviations, and a coverage check. Column headers written in Spanish (Dic-25, Ene-25, Abr-25, Ago-25, Set-25) now map to months like their English equivalents. Previously such a column was dropped from headers and from every line_items entry, so a statement’s monthly figures could silently under-report the year by a month. A new t12_column_coverage validation check reports warning whenever a column that carried data was dropped because its header could not be interpreted — t12_month_mapping only describes the columns that were read, so it could not surface this.
  • Long polling and richer job status. GET /v1/extractions/{id} and GET /v1/underwritings/{id} accept ?wait= (0-45 seconds), holding the request until the job finishes so you don’t need a sleep loop; a wait that elapses returns the in-flight state as a normal 200. In-flight responses now include poll_after_seconds (mirrored in a Retry-After header) and elapsed_seconds, and rent-roll documents report phase (extracting → consolidating) so two-phase jobs are visibly progressing. The same wait argument is available on the get_extraction and get_underwriting MCP tools. All additive — existing polling code is unaffected.
  • MCP server. https://api.trycactus.com/mcp exposes the API as tools for Claude, Cursor, and any other MCP client, authenticated with your existing API key. See MCP Server.
  • Typed T-12 and offering memorandum results. T-12 extractions now return t12.v1: report metadata, the statement’s line items with every monthly value and its source cell, and a headers array typing each spreadsheet column as a calendar month, a named total, or unknown. Filter to type: month for the trailing-twelve series — statements routinely carry extra columns, and a column is only called a total when the document names it one. Offering memorandum extractions now return offering_memorandum.v1: every extracted fact organized into the document’s own sections, plus a property summary of the key property attributes. Extractions that produce no data at all now fail and are refunded in full instead of returning an empty result.
  • Typed rent roll results. Rent-roll extractions now return rent_roll.v1: a normalized unit table (typed headers, one entry per unit) and the as-of date. If the structured result cannot be produced, the extraction fails and the charge is refunded in full — you are never billed for output you didn’t receive.
  • Initial release. Documents, extractions, usage, and webhook endpoints are live. Other document types return the facts.v1 schema — extracted values with confidence scores and source locations.