Investorlift DS
  • Getting started
  • The contract
  • Errors
  • API reference
IdempotencyRate limitsGlossaryReading a scoreThe History CallBatch scoringVersioning & deprecation
The contract

Reading a score

A Scored Call returns two kinds of number: what the pricing model estimates, and what the recorded comparable sales imply. They are different evidence, they move for different reasons, and reading them as one number is the mistake this page exists to prevent. The member-by-member schema, with the full description of every field, is in the API reference — this page is about what the numbers are and are not.

Two numbers, two sources

  • pricing_model_arv_estimate is read from the pricing model's own value distribution. It does not move when comparable sales are added or removed.
  • comp_band is derived from the comparable sales themselves, size-normalised to the property where their square footage allows it. It is the number that does move with the comps.

If you display both, expect exactly one of them to change when the comparable evidence changes. That is not a bug in either number; it is what each one is.

When the model's estimate is withheld

model_estimate_available is always present, alongside subject_property. When it is false, the model's estimate and its distribution are withheld from the response: they were checked against the comparable sales and did not agree with them, and a confidently wrong valuation is worse than none. The Comp Band and the comparable sales are unaffected.

A response with model_estimate_available: false is a charged 200. The Comp Band it ships with is a valuation — derived from recorded sales, size-normalised, stated with its evidence — so the call did what a Scored Call is for. One Credit was spent, exactly as for any other Scored Call.

subject_property — which house these numbers are about

Every scored answer names the property it resolved to and the facts the valuation ran on. A confident number about the wrong house looks exactly like a confident number about the right one, so read this block before you act on the rest of the response.

This block is additive: an integration that ignores it is completely unaffected. Nothing else in the response changed when it was added.

It carries four things.

1. Which property. property_id is the property data service's identifier for the house that was scored, and address is that record's address as the county stores it — not the address you submitted. Echoing your own text back would prove nothing; the resolved record is the only thing that shows which house was priced. The stored city is lower-cased by the property data service, and it is published unchanged rather than re-cased into something the county does not hold. A component the record does not carry is left out, and address is omitted altogether when the record carries none of them — property_id is the identifier that is always present.

2. The seven facts. Under facts:

FactWhat it is
square_feetFinished living area, in square feet.
bedroomsBedroom count.
bathroomsThe bathroom count as the property data service calculates it, published as stored. It can be fractional — 2.5 is ordinary here, not an edge case.
partial_bathroomsThe partial-bathroom count the record stores separately. The word partial is the record's own term and does not necessarily mean a half bath.
year_builtThe year the structure was built.
lot_size_square_feetLot size, in square feet — never acres.
property_typeThe property type in the county record's own wording.

bathrooms and partial_bathrooms are two separate stored figures, and this service does not add the two together. Each is published exactly as the property data service holds it. We do not claim what the calculated bathroom count already includes, so do not read the two as a sum — a record reporting 2.5 and 1 is not a house with three and a half bathrooms.

Each one is a { value, source } pair, and source is one of exactly two values: county-record, meaning the property data service's stored value for this property, or caller-stated, meaning a value you supplied in its place using the overrides request option described below. A fact you did not supply reads county-record, and partial_bathrooms reads county-record on every answer, because it cannot be overridden.

A fact that is missing means the county record does not carry it. That is not an error, and it is not "not applicable": the fact is left out of facts entirely rather than returned as null, 0 or an empty string, because the valuation ran without it too. Absence is information — it tells you the number you are reading was produced without that fact. A recorded living area of zero is read as no living area at all — zero square feet is not a building — so that property is scored without square footage and the block says so by leaving square_feet out.

3. How far the address was confirmed. address_quality carries a code you can branch on and a statement you can show someone. It is not a confidence score and not a probability. Getting an answer at all already required your house number to match exactly one complete county record; this reports whether the ZIP code agreed as well.

CodeWhat it meansWhat it does not mean
zip-confirmedYour address resolved to a readable ZIP code, and the matched county record sits in the same one.Not a guarantee the record is correct or current — only that two specific checks agreed.
zip-unconfirmedEither no readable ZIP code was available for your address, or the county record does not carry that same ZIP code — a different one, or none at all.Not a failed match. The house number still matched exactly one complete county record.

The ZIP code being compared is the one your address resolved to, which is not always one you typed — a query with no ZIP code in it often still resolves to one.

4. Whether the property is outside the model's range. outside_model_range is always present, flagged or not, and carries flagged plus a reasons list that is empty when nothing applies. It is a caution about the estimate, not a claim about the house: when it is flagged, read the numbers in this answer with extra scrutiny.

ReasonWhat it means
large-lotThe lot is larger than the sizes the pricing model is calibrated over.
large-structureThe finished living area is larger than the sizes the model is calibrated over.
property-typeThe property type the valuation ran on — the county record's, or yours if you overrode it — is one the model does not cover well: multifamily, commercial, mixed-use or industrial.

A property can carry more than one reason. A fact neither the county record nor your overrides supplies cannot raise its own reason: an unknown lot size is not a large lot. The size thresholds are policy figures, calibrated by judgment and intended to be tuned.

These reasons are read from the facts the valuation actually ran on, so a fact you stated yourself can raise one — state a five-acre lot and you will see large-lot. That is deliberate: the caution is about the estimate you are being handed, and that estimate was computed from your values.

overrides — pricing the property as you describe it

A county record is not always right, and it is often not current. When a seller tells you the basement is finished, the addition is done, or the lot is bigger than the record says, send overrides and the property is priced as you describe it — and the answer says plainly which facts were your claim and which were the county's.

It is additive. An integration that does not send overrides is completely unaffected: same request on the wire, same answer, same labels, same cost. Sending it never changes what a call costs — it is a request option, not a second property, so a call with a what-if is one Credit exactly as it was before.

The six facts you can override

FactUnits and rules
square_feetFinished living area, in square feet. Must be a number above zero.
bedroomsBedroom count. A number, zero or above — 0 is accepted, because a studio genuinely has none.
bathroomsBathroom count, as the property data service counts them. A number, zero or above, and may be fractional — 2.5 is ordinary.
year_builtThe year the structure was built. A number above zero.
lot_size_square_feetLot size, in square feet — never acres. A number above zero.
property_typeThe property type, as free text. Must not be blank or only spaces; surrounding spaces are trimmed, exactly as the county record's own value is, so the trimmed value is what appears in facts.
Code
{ "address": "742 Evergreen Terrace, Springfield, IL 62701", "overrides": { "square_feet": 2400, "bathrooms": 2.5 } }

The coordinates are never overridable, and that is a rule

A what-if changes the house's facts, never which house. latitude and longitude are not on the list above and never will be, so there is no way to ask this API for a valuation of one property under another property's address. property_id and the address in subject_property always describe the house the county record matched.

partial_bathrooms is not overridable either, so its source reads county-record on every answer. And note that overriding bathrooms leaves partial_bathrooms at the county's figure — this service still does not add the two together, so do not read your own stated bathroom count plus a county partial count as one total.

What caller-stated means

Every fact in subject_property.facts carries a source. A fact you supplied through overrides reads caller-stated; everything else reads county-record.

A value you supply reads caller-stated even when it matches the county record's own figure. The label says where the number came from, not whether it changed — so a claim can never be mistaken for a record, and a record is never quietly relabelled as a claim.

Anything we cannot accept is refused before you are charged

overrides is checked before any Credit is spent, so a rejected what-if costs nothing. These are all refused with a 400:

  • any key that is not one of the six above — including latitude, longitude, partial_bathrooms, address, or a misspelling. Unrecognised keys are refused, never silently ignored, so a typo can never leave you believing a claim was priced when it was not;
  • a value of the wrong type — a string where a number belongs, an object, an array, true;
  • null for any fact. An override may state a value; it can never erase one;
  • a square footage, lot size or year at or below zero, a negative bedroom or bathroom count, or a blank property_type;
  • overrides: {}. An empty what-if states no claim, and its answer would be indistinguishable from a call that sent no overrides at all.

Everything above is refused identically by POST /v1/validate, which costs nothing and makes no charge of any kind — so you can check a body before you spend a Credit on it.

The required facts are still required

A valuation needs the property's coordinates, its lot size and its property type. If the property still lacks one of those after your values are applied, the call is refused and the Credit is refunded, exactly as an ordinary request would be. There is no "score it anyway": a confident number built on facts the model never received is the one thing this API will not sell you.

The useful consequence runs the other way too. lot_size_square_feet and property_type are both required and overridable, so a value you supply can make a property scorable that the county record alone could not score — and on that answer, the fact that rescued it reads caller-stated.

Pricing on your own numbers

The repair budget and the target margin behind a score are ours by default. Both can be yours instead. Both inputs are additive: an integration that sends neither is completely unaffected, and every response looks exactly as it did before they existed — plus the two echoes described below.

rehab_budget — your repair cost, in whole dollars

Send rehab_budget and the repaired value and the maximum allowable offer are built from your figure instead of the bracket condition maps to. Accepted range: 0 to 10,000,000, whole dollars only — 48000, never 48000.50.

  • It overrides condition entirely. Sending both is legal and documented, and the budget wins every time. You never have to pick the bracket that comes closest to a number you already know.
  • Zero is a real answer. A house you have walked and found needs no work is rehab_budget: 0, and it prices exactly as turn_key does.
  • condition: tear_down is still refused with a 400, even with a repair budget. A repair budget does not answer what a tear-down needs, which is a land value — and there is no land-value estimate behind these numbers. Analyze lot value separately.

target_margin — the margin your buyer needs, as a fraction

Send target_margin and the maximum allowable offer is solved at your margin. 0.2 means twenty percent. Accepted range: 0.01 to 0.9. The default is 0.2.

Both are echoed back, on every scored answer

rehab_budget and target_margin in the response are the numbers the arithmetic actually used — yours when you sent them, ours when you did not. They are present either way, deliberately: otherwise "what margin is this price built on?" would be unanswerable on exactly the calls where you assumed the default.

Neither input changes what a call costs. They are request options, not properties: one property is one Credit whether you send both, one or neither.

max_allowable_offer — the ceiling, and the one flag to read beside it

max_allowable_offer is the highest purchase price at which a cash flip buyer on standard financing still clears the target margin. It is the algebraic inverse of a flip pro forma rather than a price quantile, so a seller's suggested price and a buyer's underwriting agree by construction.

This is the investor's ceiling — your offer to the seller is this minus your fee.

It is not a prediction of what the property will trade at, and it is not an offer to make as written. It is the number above which the deal stops working at the margin you asked for.

Code
"max_allowable_offer": { "amount": 187000, "clamped": false }
MemberWhat it is
amountThe price, in whole dollars, rounded to the nearest thousand.
clampedtrue when no purchase price at all clears the target margin on this deal, at this repair budget.

clamped is the no-price-works alarm, and it is the flag to branch on. When it is true the amount beside it is not an offer and must not be presented as one — a zero-dollar price is not a price. It is computed from the raw solve before any rounding, so a deal that solves to a small negative number is still reported as clamped rather than rounded quietly to zero. Read it on every answer before you act on the amount.

Both members are present on every answer that carries a price, and the block is absent whenever the Comp Band is absent.

max_allowable_offer is the name on every path that returns a price. There is one scoring vocabulary and one version of it, and no second shape to choose between; see the Versioning page.

cost_stack — the deal behind the price

cost_stack breaks the maximum allowable offer into the line items a flip buyer would actually carry. It is present on exactly the answers that carry a price.

LineWhat it is
down_paymentThe cash the buyer puts into the purchase.
loanThe borrowed balance — the maximum allowable offer less the down payment.
interestInterest on that loan over the hold period below.
pointsLending points charged on the loan.
hold_daysHow long the deal is modelled as being held. It grows with the repair budget, because a heavier renovation takes longer.
closing_costsClosing costs, charged on both the purchase and the resale.
target_profitThe profit the offer was solved to leave on the table — your target_margin of the rehab-adjusted ARV, taken before that ARV was rounded for publication. Multiply the published rehab_adjusted_arv by target_margin yourself and you will land close to this figure but not exactly on it; the difference is the ARV rounding, and the price was solved against the unrounded value.

These are whole dollars, and down_payment plus loan is the maximum allowable offer exactly. Rounding is applied once, above both the price and these lines, so those two columns always add back to the headline — on every answer, including the ones where no price could be solved.

When a price was solved, the whole stack reconciles. On an answer where max_allowable_offer.clamped is false, add down_payment, loan, rehab_budget, interest, points, closing_costs and target_profit and you land back at rehab_adjusted_arv. The gap is rounding and nothing else — the price to the nearest thousand, rehab_adjusted_arv to the nearest thousand, and each remaining line to the nearest dollar — so it stays a little over a thousand dollars across the repair budgets this API brackets. It grows with a very large repair budget, because a longer hold puts more financing cost on each rounded dollar of price.

When clamped is true, there is nothing to reconcile. No price in range cleared your target margin, so the published price is a floor rather than a solution. The lines still describe that published price honestly, but they will not add back to rehab_adjusted_arv, and the difference can be large. Read clamped first on those answers — it is what tells you the stack describes a deal nobody can do.

Every figure here follows from the financing assumptions behind the maximum allowable offer, which are policy figures calibrated by judgment and intended to be tuned — not a quote from any lender, and not fitted to a data set.

as_is_similar_band — what the comparable sales carry, per square foot

comp_band is a range of total dollars for this property. as_is_similar_band is a different cut of the same evidence: a range of dollars per square foot, taken over the comparable sales listed in comps.

It feeds nothing. No other number in the response is computed from it — not the Comp Band, not the rehab-adjusted value, not the maximum allowable offer, not the check that withholds a model estimate. It is evidence you can read beside those numbers, and that is all it is.

Code
"as_is_similar_band": { "price_per_square_foot": { "low": 171, "median": 186, "high": 204 }, "qualifying_sales": 5, "basis": "similar-sales", "statement": "This price-per-square-foot range covers only the comparable sales below that sold recently and are within the size and age tolerances this service applies. `qualifying_sales` is how many of them passed that filter." }

The three filters

A comparable sale qualifies when all three hold. Each bound is a setting of this service, and the values it currently ships with are:

FilterWhat it meansShipped valueWhere it comes from
SizeThe sale's square footage is within this fraction of this property's square footage±15%Fixed by the requirement this block implements
AgeThe sale's build year is within this many years of this property's±20 yearsFixed by the requirement
RecencyThe sale was recorded within this many days before the request365 daysThis service's own choice, not fixed by the requirement

Both the size and the age fences are inclusive — a sale exactly at the bound is inside it. A sale whose recorded date is missing, or is not a plain YYYY-MM-DD date we can read, does not qualify: it is not treated as recent, and it is not treated as old.

If this property's own square footage or build year is missing from the county record, there is nothing for the size or age fence to measure against, so no sale qualifies — and you get the fallback below. Nothing is substituted for the missing figure.

The fallback is a stated outcome, not an edge case

Thin filters are ordinary. When fewer than 3 sales qualify (again, a setting of this service), the block does not go quiet and does not pretend:

  • basis flips from similar-sales to all-comparable-sales;
  • the three filters are dropped, and the range is recomputed over every sale in comps that carries a usable sale price and square footage;
  • statement changes to say plainly that too few similar sales were found;
  • qualifying_sales still reports how many passed the FILTER — so on the fallback it is a number below 3. That is the point. It is not the size of the comparable list.

How to tell which one you got: read basis. Do not parse statement — it is written for a person to read and its wording may change. basis is a closed set of exactly two values and it is what to branch on.

The fallback stays a price-per-square-foot range. It never becomes the dollar comp_band; those are two different units and they are never published under one name.

When the block is absent

If no comparable sale carries both a usable sale price and a usable square footage, the member is absent — not present-and-empty, and not null. No member inside it is ever null. The block is present on answers where the model estimate was withheld, for the same reason comp_band and comps are: it is computed from the recorded sales, not from the model.

What each comparable sale carries

Every entry in comps carries the sale's address, date, price, size, beds, baths and build year — and six further facts.

FieldWhat it is
property_idThe property data service's identifier for that property. It is what a property-history call takes — see below.
similarity_rankThis sale's position in the comps list, starting at 1.
price_per_square_footThe sale's price divided by its square footage, in dollars.
distance_milesStraight-line distance from this property to that one, in miles, to two decimals.
lotsizeThe lot size recorded for that property. The unit is not stated — see below.
property_typeThe property type recorded for that property, republished word for word.

What similarity_rank actually is

It is a position, not a score. Rank 1 is the sale the scoring service returned first; rank 2 is the one it returned second. The comps array has always been "in the order the scoring service ranked them", and this field simply makes that order readable without counting array positions.

It is not a similarity percentage, and rank 1 is not a claim that the sale is the most comparable one. That service does not publish how it ordered them, so neither do we.

Ranks are computed after this property's own prior sale is dropped and after the list is capped, so they always run 1, 2, 3… with no gaps.

lotsize — why it has no unit, and no _square_feet in its name

The lot size for a comparable is republished under the field name the upstream data carries, and this service has not established what unit that figure is in. Naming it lot_size_square_feet would assert something nobody here has measured, so it is not named that. Compare lot sizes against each other within one answer; do not convert them against a unit you have assumed. (subject_property.facts.lot_size_square_feet is in square feet — that one is established, and it is a different field from a different source.)

Missing values are left out, never sent as null

A sale missing a coordinate carries no distance_miles. One missing square footage carries no price_per_square_foot. One whose price was recorded as a nominal amount — a quitclaim or a transfer between relatives — carries neither sale_price nor price_per_square_foot, and contributes to neither the range nor the qualifying count. There is no zero-filled lot size, no "unknown" property type and no distance of 0 standing in for "we don't know".

property_id — the key to a property’s history

Every comparable carries a property_id, the same kind of identifier subject_property.property_id carries for the property you asked about. It is the input a property-history call takes — the request that returns the recorded transaction story behind a comparable: what it last sold for, what kind of buyer bought it — an individual, a company or a trust — and whether it was a flip. It reports the kind of buyer, never a name.

That call is the History Call. property_id is what you hand it, so it is worth keeping alongside any comparable you want to follow up on.

What is still never published

The comparables' latitude and longitude are not published, and publishing a distance does not change that — those coordinates identify someone else's parcel to the metre. The scoring service's internal similarity score is not published either: it is a number we cannot explain, and a number you cannot interpret is worse than no number.

What is verified, and what is not

Stated plainly, because the distinction matters.

Verified:

  • The derived outputs are internally consistent and deterministic: the same response yields the same derived numbers, exactly.
  • The maximum allowable offer is the algebraic inverse of a flip pro forma, and the round-trip between them holds to the cent — a seller's suggested price and a buyer's underwriting agree by construction.
  • The check that withholds a model estimate disagreeing with the comparable sales (the model_estimate_available: false path above) is exercised and tested.

Not verified:

  • There is no published back-test of predicted price against realized sale price. Not for the model's estimate, and not for the derived maximum allowable offer. If accuracy matters to your use case, budget for your own back-test.
  • The target margin and the financing figures behind max_allowable_offer are policy figures calibrated by judgment and intended to be tuned. They are not fitted to any data set.

Accuracy caveats that survive every release

  • Residential only. Large multifamily and commercial properties are outside what the model covers, and will diverge rather than error.
  • Tear-downs are unsupported. There is no land-value estimate; analyze lot value separately.
  • Repair estimates are screening ranges, not bids. Small homes skew high per square foot, and heavy-rehab figures are the least reliable in the set.
  • The band is only as good as the comps. Show effective_sample_size alongside any band you publish: a low value means the number rests on thin evidence and should be read with caution.
  • Sparse property data yields confident-looking numbers. A value computed from three missing inputs still renders with the same number of digits. The evidence members exist so you can tell the difference — use them.
Last modified on September 1, 2026
GlossaryThe History Call
On this page
  • Two numbers, two sources
  • When the model's estimate is withheld
  • subject_property — which house these numbers are about
  • overrides — pricing the property as you describe it
    • The six facts you can override
    • The coordinates are never overridable, and that is a rule
    • What caller-stated means
    • Anything we cannot accept is refused before you are charged
    • The required facts are still required
  • Pricing on your own numbers
    • rehab_budget — your repair cost, in whole dollars
    • target_margin — the margin your buyer needs, as a fraction
    • Both are echoed back, on every scored answer
  • max_allowable_offer — the ceiling, and the one flag to read beside it
  • cost_stack — the deal behind the price
  • as_is_similar_band — what the comparable sales carry, per square foot
    • The three filters
    • The fallback is a stated outcome, not an edge case
    • When the block is absent
  • What each comparable sale carries
    • What similarity_rank actually is
    • lotsize — why it has no unit, and no _square_feet in its name
    • Missing values are left out, never sent as null
    • property_id — the key to a property’s history
    • What is still never published
  • What is verified, and what is not
  • Accuracy caveats that survive every release
JSON
JSON
JSON