Skip to main content
POST
Start an automated underwriting run

Authorizations

Authorization
string
header
required

Partner API key issued by Cactus.

Headers

Idempotency-Key
string

Unique key making the request safely retryable.

Maximum string length: 255

Body

application/json
property
object
required
documents
object[]
required

The uploaded documents to underwrite from, each with the role it plays. Required — a run reads the property's own documents, so there is no address-only mode.

Everything that decides whether a run can start is checked here, not thirty minutes in: the document exists and has a file, its role is unambiguous, no role is claimed twice, and the file's format is one that role can be read from. Rejections are 422 and are never billed.

Required array length: 1 - 5 elements
notification_email
string<email>

Optional. With public_link delivery (the default), receives an email with the public report link when the run completes. Omit it to send no email; the link is still returned as report.url. Private delivery and sandbox runs send no email.

end_user_ref
string

Your stable opaque identifier for the customer this underwriting belongs to. Reuse it across that customer's underwritings; each run receives its own underwriting ID. References are scoped to your partner account and map to separate app organizations. GET /v1/underwritings?end_user_ref= filters your runs by owner. Use an internal ID rather than an email.

This is an ownership identifier, not an authentication credential. Your API key authorizes partner-level access. Your backend must authenticate its users and check ownership before returning a run to them. The reference does not restrict access to a public report.url share link.

Omit it only when underwriting on your own behalf. If your account is configured to require it, omitting it returns 422 (end_user_ref_required).

Required string length: 1 - 255
options
object

Response

Run queued.

id
string
required
Example:

"uw_01J9ZK3M"

object
string
required
Allowed value: "underwriting"
status
enum<string>
required

queued → ingesting (reading the documents and the market) → underwriting (analysis) → calculating (the financial model) → completed. progress.stage carries the finer-grained pipeline step behind each.

Available options:
queued,
ingesting,
underwriting,
calculating,
completed,
failed
sandbox
boolean
required
created_at
string<date-time>
required
progress
object
property
object

Echo of the submitted property.

end_user_ref
string

Echo of the submitted end-user identifier, when one was supplied.

version
integer

Report version. 1 for a first run; a confirmed change creates a new underwriting with the next number.

previous_underwriting_id
string

The version this one changed (absent on version 1).

documents
object[]

Echo of the submitted selection, with each role settled.

options
object

Echo of the submitted options, with defaults applied.

billed_total_usd
string
completed_at
string<date-time>
elapsed_seconds
integer

Whole seconds since created_at. Present while the run is active, so a caller can tell a run that just started from one approaching the 4-hour ceiling without doing date math.

Example:

640

poll_after_seconds
integer

Recommended seconds to wait before polling again. Present while the run is active; mirrors the Retry-After header. Prefer this over a hard-coded interval.

Example:

30

report
object

Present when completed. Private delivery omits url.

summary
object

Headline metrics as plain numbers, lifted out of results for convenience: value_base_usd, cap_rate, normalized_noi_usd, levered_irr, dscr, equity_multiple, total_units and so on, plus readiness_status and disclosure_count.

A metric the run could not establish is absent, never null — the reason it is missing is in results, beside the figure.

results
object

The whole report, in the same shape the public report page is rendered from (broker-public-underwriting-delivery-v1). Present on a completed run when fetched individually; omitted from list responses and webhook payloads, which would otherwise carry megabytes.

Every figure is a {state, value, reason} triple, because the product distinguishes a known zero from something it could not establish. state is one of known, zero, unavailable, not_applicable; when it is not known or zero, value is null and reason says why in a sentence you can show a user. Collections carry {state, items, reason}, where state is available, partial, confirmed_empty or unavailable — "we found none" and "we could not look" are different answers.

Top level: schema_version, generated_at, property, chapters, critical_assumptions, readiness, release.

The chapters follow the argument the report makes: answer (what it is worth, and the cap rate and NOI behind that), income (unit mix and rent comparables), revenue (the trailing-twelve income statement and the year-one revenue waterfall), cost (the expense statement and the normalizations applied to it), value (sale comparables and a cap-rate sensitivity grid), buyer (returns, financing and the annual cash-flow table), risk (market watch), next (talking points).

readiness.disclosures is the part to surface next to any figure you quote: each names something the run had to assume or could not establish, with copy written to be shown to an end user. critical_assumptions.items ranks the assumptions the valuation is most sensitive to, with the dollar and IRR impact of moving each.

error
object