Investorlift DS
  • Getting started
  • The contract
  • Errors
  • API reference
Information
MCP
    MCP Endpointpost
Scoring
    Score a property addresspostScore a batch of property addressespostCheck a request without spendingpost
History
    Read a property’s recorded transaction historypost
Schemas
Investorlift Data Services API
Investorlift Data Services API

Scoring


Score a property address

POST
https://api.investorliftdata.com
/v1/predict

Scores a single property address and returns a rehab-aware valuation and the maximum allowable offer — the most a flip buyer on standard financing can pay and still clear the target margin.

This is a Metered Call. The Credit cost is resolved by the Ledger inside the same transaction that records the Spend; it is never sent by the client. The Spend is committed before the scoring service is called, so a call that reaches the scoring service has already been paid for, and a call that fails before the Spend consumes nothing.

Send an Idempotency-Key to make a retry safe. A key is replayed only when the earlier call succeeded and its response was stored. If the earlier call was refunded, ended without a stored response, or is older than the replay window, the key is taken over: the new request runs and is charged, whatever body it carries. Otherwise the same key sent with a different body is refused as a conflict, and nothing is charged for it.

POST /v1/validate is the validation endpoint for this call, and it is free.

Score a property address › Headers

Authorization
​string · required

The Authorization header is used to authenticate with the API using your API key. Value is of the format Bearer YOUR_KEY_HERE.

Idempotency-Key
​string

An opaque string of your choosing that makes retrying this request safe. Optional — a request without one is never an error, and is charged per call. Reusing a key with the same body within 24 hours returns the stored result and is not charged again, but only when the earlier call succeeded and its response was stored. Only successful responses are stored: if the earlier call was refunded or ended without a stored response, the key is taken over and the new request runs and is charged, whatever body it carries. Otherwise reusing a key with a different body is rejected as a conflict, and nothing is charged for it. The replay window is 24 hours, measured from the request that produced the stored result; after 24 hours the same key is a new request and is charged. If an identical request carrying the same key is still in flight, this one waits briefly for that result; if it does not arrive, the response is 409 with a Retry-After header and no Credits are consumed for it. Keys are scoped to your Account — one Account never replays another Account's result. POST /v1/validate ignores this header: it is not a Metered Call, so there is nothing to replay.

Score a property address › Request Body

Exactly one property, plus optional request options. `maxProperties` is the documented form of the cap these endpoints enforce, and it counts the property together with every option this endpoint accepts. `condition`, `rehab_budget`, `target_margin` and `overrides` are options, not properties — sending any or all of them never changes what the call costs. Every OTHER body member counts as a property, so a mistyped member name is counted as one: a one-property body carrying it returns 413 with `submitted` one higher than the properties you meant to send, and the fix is the member name, not a smaller batch. `gateway/tests/route-surface.test.ts` asserts that `maxProperties` equals the route's `maxSubmittedProperties` option plus the number of options it declares, so the published cap cannot drift from the enforced one.
ScoredCallRequest
address
​string · required

The full street address of the property to score.

Example: 742 Evergreen Terrace, Springfield, IL 62701
condition
​string · enum

The property's condition, which sets the repair budget the derived numbers are built on. Optional: omit it and the property is priced as though it needs no repairs, which is exactly what turn_key does. It is a request option rather than a second property, so sending it never changes what the call costs.

tear_down is accepted by this schema and refused by the endpoint with a 400, before any Credits are consumed. That is deliberate rather than an oversight: the scoring service has no land-value estimate, so there is no honest way to price a tear-down here. Analyse lot value separately.

Repair figures are a screening range, not a contractor bid.

Enum values:
turn_key
light_rehab
major_rehab
full_gut
tear_down
Example: major_rehab
Default: turn_key
rehab_budget
​integer · min: 0 · max: 10000000

Your own repair budget for this property, in whole dollars. Optional, and additive: an integration that does not send it is unaffected.

When you send it, the repaired value and the maximum allowable offer are built from your figure instead of the bracket condition maps to, and the rehab_budget in the response is the number the arithmetic actually used. Sending rehab_budget and condition together is legal — rehab_budget wins, every time — so you never have to choose the bracket that comes closest to a number you already know.

condition: tear_down is still refused with a 400 even when you send 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.

Repair figures are a screening range, not a contractor bid.

Example: 48000
target_margin
​number · min: 0.01 · max: 0.9

The profit margin your buyer needs, as a fraction of the repaired value — 0.2 means twenty percent. Optional, and additive: an integration that does not send it is unaffected, and is priced at the default of 0.2.

When you send it, the maximum allowable offer is solved at your margin rather than the default one. The target_margin in the response is always the margin the answer was priced at, whether you sent one or not, so "what margin is this price built on?" is answerable on every response.

The target margin is a policy figure calibrated by judgment, not a parameter fitted to a data set — which is exactly why you can override it.

Example: 0.25
Default: 0.2
​object · minProps: 1

Price the property as you describe it, not as the county record has it. Optional, and additive: an integration that does not send it is unaffected and its answers do not move.

Send any of the six facts below and that value is used in place of the county record's, for the model, for the comparable-sales band and for the price derived from it. Every fact in subject_property.facts says where it came from, so a value you stated reads caller-stated and everything else reads county-record — a claim can never be mistaken for a record.

A value you supply reads caller-stated even when it matches the county's own figure. The label says where the number came from, not whether it changed.

Only these six facts, and nothing else. Any other key — including latitude, longitude, partial_bathrooms, address, or a misspelling of one of the six — is refused with a 400 before any Credit is spent, rather than being ignored. The coordinates are never overridable, and that is a rule rather than an omission: a what-if changes the house's facts, never which house.

Values must be usable, and an empty overrides is refused. A value must be the right JSON type, and must satisfy the rule stated on each fact below; null is refused, because an override may state a value but never erase one. overrides: {} is refused too — it declares a what-if while stating no claim, and its answer would be indistinguishable from a call that sent no overrides at all. Every one of these is a 400 before any Credit is spent.

The required facts are still required. If the property still lacks a fact the valuation needs 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". Your lot_size_square_feet or property_type can supply a fact the county record was missing, in which case the call scores and that fact reads caller-stated.

Overriding bathrooms does not change partial_bathrooms, which stays at the county's figure and is not overridable. This service does not add the two together, so do not read your stated bathroom count plus a county partial count as one total.

Example: {"square_feet":2400,"bathrooms":2.5}

Score a property address › Responses

The Scored Call succeeded. This was a Metered Call, so Credits were consumed from the Account's Balance, as resolved by the Ledger.

The underwriting answer for the submitted property. This body is built by Investorlift Data Services member by member; the scoring service's own response is never passed through. The member set below is closed: nothing else ever appears, and a member is added only as a documented, non-breaking addition. Every response also carries the Trace ID as the `trace-id` header — it is deliberately not repeated in this body, because a stored answer replayed under a later `Idempotency-Key` would otherwise carry the original call's handle and point support at the wrong call. **How well these numbers perform, stated honestly.** The distinction below matters more than any single figure, so it is published rather than left to be discovered. **What is verified.** The arithmetic is internally consistent and deterministic: the maximum allowable offer is the exact algebraic inverse of the flip pro forma it is solved from, verified to the cent, so a seller's suggested price and a buyer's underwriting agree by construction. The model's estimate is checked against the comparable sales before it is published, and withheld when the two disagree. **What is not verified.** There is **no published back-test of these numbers against realised sale prices** — not for the model's estimate, and not for the maximum allowable offer derived from the comparable sales. If accuracy matters to your use case, budget for your own back-test. **The policy figures.** The repair-lift thresholds, the target margin and the financing assumptions behind the maximum allowable offer are **policy figures, calibrated by judgment and intended to be tuned**. They are not parameters fitted to a data set, and they are not presented as if they were. **Coverage and caveats.** Residential property only: large multifamily and commercial subjects are outside coverage and will diverge rather than return an error. Tear-downs are unsupported — there is no land-value estimate behind them — so analyse lot value separately. Repair figures are a screening range, not a contractor bid. The model's estimate does not move when comparable sales are added or removed; the Comp Band does, and the Comp Band is what the maximum allowable offer is built on. A band computed over an uncurated set can be pulled by a single non-comparable sale, so read `effective_sample_size` alongside it. Property data is sparse in places, and a confident number computed from thin inputs still looks confident.
ScoredCallResponse
model_estimate_available
​boolean · required

Whether the pricing model produced an estimate Investorlift Data Services was able to confirm for this property. Always present, alongside subject_property. When false, the model's estimate and its distribution are withheld from this response: they were checked against the comparable sales below and did not agree with them, and a confidently wrong valuation is worse than none. The Comp Band and the comparable sales are unaffected — they are derived from the sales themselves — and this call is charged as normal, because it still carries a valuation.

Example: true
​object · required

The property this answer is about, and the facts the valuation actually ran on. Always present on a scored answer, including one where the model's estimate was withheld. It is here so a mis-resolved address or a thin county record is visible before you act on the numbers: check that the address and the facts below describe the house you meant.

Every fact carries where it came from. county-record means the property data service's stored value for this property; caller-stated means a value you supplied in its place. Those are the only two values. A fact reads caller-stated when you supplied it through the overrides request option, and county-record otherwise — including partial_bathrooms, which cannot be overridden. A value you supply reads caller-stated even when it matches the county record's own figure.

A fact that is missing is missing on purpose. When the county record does not carry a usable value for something, that fact is left out of facts entirely rather than returned as null, 0 or an empty string. Absence is information: the valuation ran without it too. A recorded living area of zero is treated as no living area, not as a house that measures nothing.

This block is additive. It was added without changing any other member, and an integration that ignores it is unaffected.

Example: {"property_id":"a1b2c3d4e5f60718293a4b5c6d7e8f90","address":{"street":"2030 Lone Rock Dr","city":"kingwood","state":"TX","zip":"77345-1234"},"facts":{"square_feet":{"value":2416,"source":"county-record"},"bedrooms":{"value":4,"source":"county-record"},"bathrooms":{"value":2.5,"source":"county-record"},"partial_bathrooms":{"value":1,"source":"county-record"},"year_built":{"value":1998,"source":"county-record"},"lot_size_square_feet":{"value":8276,"source":"county-record"},"property_type":{"value":"Single Family","source":"county-record"}},"address_quality":{"code":"zip-confirmed","statement":"The house number matched exactly one county record, and that record sits in the same ZIP code your address resolved to. Check the address and property facts in this block before relying on these numbers."},"outside_model_range":{"flagged":false,"reasons":[]}}
pricing_model_arv_estimate
​integer

The pricing model's after-repair-value estimate for this property, rounded to the nearest thousand dollars. Read from the model's own value distribution rather than from the comparable sales, so it does not move when comparable sales are added or removed — the Comp Band is the number that does. Absent when model_estimate_available is false.

​object

The predicted value distribution for this property, as quantile level to predicted value. The full distribution is returned rather than a single number: where the scoring service exposes its uncertainty, Investorlift Data Services surfaces it rather than hiding it. Absent when model_estimate_available is false.

valuation_mean
​integer | null

The predicted value for this property as a single point estimate. Optional: the scoring service does not always produce one, and its absence does not make the distribution above any less complete. Absent when model_estimate_available is false.

effective_sample_size
​number

How much comparable evidence supported the model's estimate. A low value means the distribution above rests on thin evidence and should be read with more caution. Absent when model_estimate_available is false, because it describes an estimate that was withheld.

Example: 72.34
​object

The Comp Band: the value range implied by the comparable sales below, size-normalised to this property where their square footage allows it. This is the number derived from recorded sales rather than from a model. Absent when too few comparable sales carried a usable price to state a range honestly — a range built on one or two sales reads as high confidence over the thinnest possible evidence.

rehab_budget
​integer

The repair budget the numbers below were built on, in whole dollars — the figure the arithmetic actually used. When you sent a rehab_budget, this is your number. When you did not, it is the whole-project midpoint for the condition you submitted, and zero when you sent no condition, which is the same as sending turn_key. A bracket figure is a screening range reduced to a single number, not a contractor bid: small homes skew high per square foot, and the heavier brackets are the least reliable in the set. Sending your own budget replaces that estimate with your own.

rehab_adjusted_arv
​integer

The after-repair value this deal is priced against, in whole dollars: the Comp Band's middle value lifted toward its upper end in proportion to the repair budget, because a heavier renovation signals a higher-finish result. It equals the middle of the Comp Band when there is no repair signal and never exceeds the upper end, so the number stays anchored to sales that actually happened. It is derived from the comparable sales rather than from the model, so it moves when the comparable sales do. The thresholds that shape the lift are policy figures calibrated by judgment, not fitted parameters. Absent whenever the Comp Band is absent.

target_margin
​number

The profit margin the maximum allowable offer (max_allowable_offer, below) was solved at, as a fraction of rehab_adjusted_arv — 0.2 means twenty percent. Always present on a priced answer, whether or not you sent a target_margin: when you sent one it is your figure, and when you did not it is the default this service priced at. It is published either way so that "what margin is this price built on?" is answerable on every response rather than only on the ones that named it. The default is a policy figure calibrated by judgment, not a fitted parameter. Absent whenever the Comp Band is absent.

Example: 0.2
​object

The maximum allowable offer (MAO): 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. The financing figures behind it — down payment, interest, points, closing costs, hold period and target margin — are policy figures calibrated by judgment and intended to be tuned; they are not fitted to any data set. Absent whenever the Comp Band is absent.

​object

The line items behind the maximum allowable offer — the deal as the flip buyer it is solved for would carry it. Present on exactly the same answers max_allowable_offer is present on, and absent whenever the Comp Band is absent.

These numbers 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 the figures you read here are the figures that add up — there is never a rounded total that disagrees with its own rounded parts. That identity holds on every answer, including the ones where no price could be solved.

When a price was solved — max_allowable_offer.clamped is false — the whole stack reconciles. 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, and 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 the target margin, so the published price is a floor rather than a solution; the lines still describe that published price, but they will not add back to rehab_adjusted_arv and the difference can be large. Read clamped first on those answers.

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.

​object

A price-per-square-foot range taken over the comparable sales listed in comps, stated as evidence beside comp_band rather than as part of the valuation. It feeds nothing: no other member of this response is computed from it. Read basis to learn which sales it covers. It is present whenever at least one comparable sale carries both a usable sale price and a usable square footage, INCLUDING on answers where the model estimate was withheld (model_estimate_available: false), because it is computed from the comparable sales rather than from the model. It is absent when no comparable sale carries both. No member of it is ever null.

​object[]

The comparable sales the Comp Band was derived from, in the order the scoring service ranked them, as stored by the property data service. The property's own prior sale is removed before anything is computed. Absent when the scoring service returned none.

POST/v1/predict
curl https://api.investorliftdata.com/v1/predict \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <token>' \ --data '{ "address": "1026 N Beckley Ave, Dallas, TX 75203" }'
Example Request Body
{ "address": "1026 N Beckley Ave, Dallas, TX 75203" }
json
application/json
Example Responses
{ "model_estimate_available": true, "subject_property": { "property_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "address": { "street": "2030 Lone Rock Dr", "city": "kingwood", "state": "TX", "zip": "77345-1234" }, "facts": { "square_feet": { "value": 2416, "source": "county-record" }, "bedrooms": { "value": 4, "source": "county-record" }, "bathrooms": { "value": 2.5, "source": "county-record" }, "partial_bathrooms": { "value": 1, "source": "county-record" }, "year_built": { "value": 1998, "source": "county-record" }, "lot_size_square_feet": { "value": 8276, "source": "county-record" }, "property_type": { "value": "Single Family", "source": "county-record" } }, "address_quality": { "code": "zip-confirmed", "statement": "The house number matched exactly one county record, and that record sits in the same ZIP code your address resolved to. Check the address and property facts in this block before relying on these numbers." }, "outside_model_range": { "flagged": false, "reasons": [] } }, "pricing_model_arv_estimate": 0, "valuation_quantiles": { "key": 0 }, "valuation_mean": 0, "effective_sample_size": 72.34, "comp_band": { "low": 0, "median": 0, "high": 0 }, "rehab_budget": 0, "rehab_adjusted_arv": 0, "target_margin": 0.2, "max_allowable_offer": { "amount": 0, "clamped": true }, "cost_stack": { "down_payment": 0, "loan": 0, "interest": 0, "points": 0, "hold_days": 0, "closing_costs": 0, "target_profit": 0 }, "as_is_similar_band": { "price_per_square_foot": { "low": 0, "median": 0, "high": 0 }, "qualifying_sales": 0, "basis": "similar-sales", "statement": "statement" }, "comps": [ { "property_id": "property_id", "address_street": "address_street", "address_city": "address_city", "address_state": "address_state", "address_zip": "address_zip", "sale_date": "sale_date", "sale_price": 0, "sqft": 0, "bedrooms": 0, "bathrooms": 0, "year_built": 0, "similarity_rank": 0, "price_per_square_foot": 0, "distance_miles": 0, "lotsize": 0, "property_type": "property_type" } ] }
json
application/json

Score a batch of property addresses

POST
https://api.investorliftdata.com
/v1/predict/batch

Scores up to the documented batch limit of properties in one request and returns one answer per property, in submission order.

Each answer carries the core numbers only. A batch entry is the same answer POST /v1/predict returns for that property, minus the list of comparable sales: the Comp Band, the qualifying count, the price-per-square-foot range and every derived figure are all here, but the individual sales behind them are not. To see them, score that property on its own with POST /v1/predict, by address: a Scored Call identifies a property by address only, and property_id is not one of the members it accepts.

This is a Metered Call: Credits are charged per property submitted, and the Credit cost is resolved by the Ledger inside the same transaction that records the Spend — it is never sent by the client. The Spend for the whole batch is committed before any property is scored, so the cost of a batch is known and paid in full before any work begins. A batch the Account's Balance cannot afford is rejected in full — never partially: nothing is charged, nothing is split, and the request can be retried unchanged after a top-up.

Credits are charged per property submitted, and refunded per property that fails for a reason the refund policy covers. A property that fails for one of the covered reasons does not stop the ones after it: its share of the batch's Credits is returned to the Account as its own Refund, and every property after it is still attempted. The response is still 200, and the failed property's entry in results is the same error object a single call would have returned for it — so an integration branching on type needs no second contract. A refusal outside those reasons stops the batch instead: the properties after it are not attempted, no results array is returned, the whole request answers 503 spend-indeterminate, and every Credit recorded for the batch is returned to the Account as a Refund. The Account is charged for every property that produced an answer and for nothing else.

Send an Idempotency-Key to make a retry safe. Keys behave exactly as on POST /v1/predict, and the same body sent to both endpoints under one key is a conflict, never a replay.

Score a batch of property addresses › Headers

Authorization
​string · required

The Authorization header is used to authenticate with the API using your API key. Value is of the format Bearer YOUR_KEY_HERE.

Idempotency-Key
​string

An opaque string of your choosing that makes retrying this request safe. Optional — a request without one is never an error, and is charged per call. Reusing a key with the same body within 24 hours returns the stored result and is not charged again, but only when the earlier call succeeded and its response was stored. Only successful responses are stored: if the earlier call was refunded or ended without a stored response, the key is taken over and the new request runs and is charged, whatever body it carries. Otherwise reusing a key with a different body is rejected as a conflict, and nothing is charged for it. The replay window is 24 hours, measured from the request that produced the stored result; after 24 hours the same key is a new request and is charged. If an identical request carrying the same key is still in flight, this one waits briefly for that result; if it does not arrive, the response is 409 with a Retry-After header and no Credits are consumed for it. Keys are scoped to your Account — one Account never replays another Account's result. POST /v1/validate ignores this header: it is not a Metered Call, so there is nothing to replay.

Score a batch of property addresses › Request Body

The batch envelope. It admits exactly the `properties` array and nothing else: any other top-level member is rejected before any Spend, including a request option the single endpoint accepts — those belong on each entry inside `properties`, never on the envelope. Credits are charged per property submitted — a request option never changes what a batch costs. `maxItems` is the documented form of the batch limit this endpoint enforces: a batch carrying more properties returns 413 with honest `limit` and `submitted` counts, before any Spend. `gateway/tests/route-surface.test.ts` asserts that `maxItems` equals the route's `maxSubmittedProperties` option, so the published limit cannot drift from the enforced one.
​ScoredCallRequest[] · minItems: 1 · maxItems: 10 · required

The properties to score, each the same shape POST /v1/predict accepts. Order is preserved: the response's results array answers these properties in this order. Duplicate addresses are allowed and each is charged — two identical entries are two work items.

Score a batch of property addresses › Responses

The batch was processed. This was a Metered Call, so Credits were consumed from the Account's Balance as resolved by the Ledger, and any property that failed for a reason the refund policy covers has had its share returned as a Refund. Individual properties may have failed — each entry in results says which.

The batch answer: one entry per submitted property, in submission order, none omitted and none reordered. An entry is EITHER a `BatchScoredCallItem` — the core-numbers answer, which is the single endpoint's answer minus the list of comparable sales — for a property that was scored, OR a Problem Detail describing why that property could not be scored and what happened to its Credit. The two are told apart by shape: only the error entry carries `status` and `type`. Both are built by Investorlift Data Services member by member; the scoring service's own responses are never passed through. The Trace ID rides the `trace-id` response header, deliberately not this body — and every entry in a batch shares the one Trace ID of the call that produced it.
​array · required

One entry per submitted property, in submission order. Entry i answers submitted property i: the position is how a failing property is identified, and no index is interpolated into any message.

POST/v1/predict/batch
curl https://api.investorliftdata.com/v1/predict/batch \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <token>' \ --data '{ "properties": [ { "address": "1026 N Beckley Ave, Dallas, TX 75203" }, { "address": "742 Evergreen Terrace, Springfield, IL 62701", "condition": "major_rehab" } ] }'
Example Request Body
{ "properties": [ { "address": "1026 N Beckley Ave, Dallas, TX 75203" }, { "address": "742 Evergreen Terrace, Springfield, IL 62701", "condition": "major_rehab" } ] }
json
application/json
Example Responses
{ "results": [ { "model_estimate_available": true, "subject_property": { "property_id": "a1b2c3d4e5f60718293a4b5c6d7e8f90", "address": { "street": "2030 Lone Rock Dr", "city": "kingwood", "state": "TX", "zip": "77345-1234" }, "facts": { "square_feet": { "value": 2416, "source": "county-record" }, "bedrooms": { "value": 4, "source": "county-record" }, "bathrooms": { "value": 2.5, "source": "county-record" }, "partial_bathrooms": { "value": 1, "source": "county-record" }, "year_built": { "value": 1998, "source": "county-record" }, "lot_size_square_feet": { "value": 8276, "source": "county-record" }, "property_type": { "value": "Single Family", "source": "county-record" } }, "address_quality": { "code": "zip-confirmed", "statement": "The house number matched exactly one county record, and that record sits in the same ZIP code your address resolved to. Check the address and property facts in this block before relying on these numbers." }, "outside_model_range": { "flagged": false, "reasons": [] } }, "pricing_model_arv_estimate": 0, "valuation_quantiles": { "key": 0 }, "valuation_mean": 0, "effective_sample_size": 72.34, "comp_band": { "low": 0, "median": 0, "high": 0 }, "rehab_budget": 0, "rehab_adjusted_arv": 0, "target_margin": 0.2, "max_allowable_offer": { "amount": 0, "clamped": true }, "cost_stack": { "down_payment": 0, "loan": 0, "interest": 0, "points": 0, "hold_days": 0, "closing_costs": 0, "target_profit": 0 }, "as_is_similar_band": { "price_per_square_foot": { "low": 0, "median": 0, "high": 0 }, "qualifying_sales": 0, "basis": "similar-sales", "statement": "statement" } } ] }
json
application/json

Check a request without spending

POST
https://api.investorliftdata.com
/v1/validate

Checks that a request body would be accepted by POST /v1/predict, and returns a verdict.

This endpoint is free. It consumes no Credits and never calls the scoring service, so it keeps working at a zero Balance. With an API Key it makes no Ledger call of any kind, so it also answers during a Ledger outage. A connected app — one that signed you in instead of holding an API Key — makes one Ledger read here to confirm the connection is still approved, so while that read is failing it answers 503 call-not-started. Nothing is charged either way. It requires a valid credential — an API Key, or an account sign-in — and nothing else.

Any Idempotency-Key header sent to this endpoint is ignored. There is no Spend to make idempotent, so no claim is recorded and no key is stored.

A verdict of false is returned only for a body this endpoint can judge on its own — shape, required fields and the property cap. The scoring service remains the authority on whether a well-formed address can actually be scored.

Check a request without spending › Headers

Authorization
​string · required

The Authorization header is used to authenticate with the API using your API key. Value is of the format Bearer YOUR_KEY_HERE.

Check a request without spending › Request Body

Exactly one property, plus optional request options. `maxProperties` is the documented form of the cap these endpoints enforce, and it counts the property together with every option this endpoint accepts. `condition`, `rehab_budget`, `target_margin` and `overrides` are options, not properties — sending any or all of them never changes what the call costs. Every OTHER body member counts as a property, so a mistyped member name is counted as one: a one-property body carrying it returns 413 with `submitted` one higher than the properties you meant to send, and the fix is the member name, not a smaller batch. `gateway/tests/route-surface.test.ts` asserts that `maxProperties` equals the route's `maxSubmittedProperties` option plus the number of options it declares, so the published cap cannot drift from the enforced one.
ScoredCallRequest
address
​string · required

The full street address of the property to score.

Example: 742 Evergreen Terrace, Springfield, IL 62701
condition
​string · enum

The property's condition, which sets the repair budget the derived numbers are built on. Optional: omit it and the property is priced as though it needs no repairs, which is exactly what turn_key does. It is a request option rather than a second property, so sending it never changes what the call costs.

tear_down is accepted by this schema and refused by the endpoint with a 400, before any Credits are consumed. That is deliberate rather than an oversight: the scoring service has no land-value estimate, so there is no honest way to price a tear-down here. Analyse lot value separately.

Repair figures are a screening range, not a contractor bid.

Enum values:
turn_key
light_rehab
major_rehab
full_gut
tear_down
Example: major_rehab
Default: turn_key
rehab_budget
​integer · min: 0 · max: 10000000

Your own repair budget for this property, in whole dollars. Optional, and additive: an integration that does not send it is unaffected.

When you send it, the repaired value and the maximum allowable offer are built from your figure instead of the bracket condition maps to, and the rehab_budget in the response is the number the arithmetic actually used. Sending rehab_budget and condition together is legal — rehab_budget wins, every time — so you never have to choose the bracket that comes closest to a number you already know.

condition: tear_down is still refused with a 400 even when you send 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.

Repair figures are a screening range, not a contractor bid.

Example: 48000
target_margin
​number · min: 0.01 · max: 0.9

The profit margin your buyer needs, as a fraction of the repaired value — 0.2 means twenty percent. Optional, and additive: an integration that does not send it is unaffected, and is priced at the default of 0.2.

When you send it, the maximum allowable offer is solved at your margin rather than the default one. The target_margin in the response is always the margin the answer was priced at, whether you sent one or not, so "what margin is this price built on?" is answerable on every response.

The target margin is a policy figure calibrated by judgment, not a parameter fitted to a data set — which is exactly why you can override it.

Example: 0.25
Default: 0.2
​object · minProps: 1

Price the property as you describe it, not as the county record has it. Optional, and additive: an integration that does not send it is unaffected and its answers do not move.

Send any of the six facts below and that value is used in place of the county record's, for the model, for the comparable-sales band and for the price derived from it. Every fact in subject_property.facts says where it came from, so a value you stated reads caller-stated and everything else reads county-record — a claim can never be mistaken for a record.

A value you supply reads caller-stated even when it matches the county's own figure. The label says where the number came from, not whether it changed.

Only these six facts, and nothing else. Any other key — including latitude, longitude, partial_bathrooms, address, or a misspelling of one of the six — is refused with a 400 before any Credit is spent, rather than being ignored. The coordinates are never overridable, and that is a rule rather than an omission: a what-if changes the house's facts, never which house.

Values must be usable, and an empty overrides is refused. A value must be the right JSON type, and must satisfy the rule stated on each fact below; null is refused, because an override may state a value but never erase one. overrides: {} is refused too — it declares a what-if while stating no claim, and its answer would be indistinguishable from a call that sent no overrides at all. Every one of these is a 400 before any Credit is spent.

The required facts are still required. If the property still lacks a fact the valuation needs 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". Your lot_size_square_feet or property_type can supply a fact the county record was missing, in which case the call scores and that fact reads caller-stated.

Overriding bathrooms does not change partial_bathrooms, which stays at the county's figure and is not overridable. This service does not add the two together, so do not read your stated bathroom count plus a county partial count as one total.

Example: {"square_feet":2400,"bathrooms":2.5}

Check a request without spending › Responses

The request body was checked. No Credits were consumed.

ValidationVerdict
valid
​boolean · required

Whether this body would be accepted by POST /v1/predict. Always true on a 200 — a body that would be rejected returns the same 400 or 413 the Metered Call returns.

POST/v1/validate
curl https://api.investorliftdata.com/v1/validate \ --request POST \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <token>' \ --data '{ "address": "1026 N Beckley Ave, Dallas, TX 75203" }'
Example Request Body
{ "address": "1026 N Beckley Ave, Dallas, TX 75203" }
json
application/json
Example Responses
{ "valid": true }
json
application/json

MCPHistory