# Batch scoring

`POST /v1/predict/batch` scores several properties in one request and returns one answer per
property, in the order you submitted them. It is the call for the moment you have a list —
ten leads to triage before standup — and you want the numbers that decide which two are worth
a closer look.

A batch is one **Metered Call** and costs **one Credit per property submitted**, the same at
every Tier. The member-by-member schema is in the [API reference](/api); this page is what
the call is, what
comes back, and — the part to read before you send one — exactly what it charges for.

## ⚠️ The answer carries the core numbers only

**A batch entry is the answer a single Scored Call returns for that property, minus the list
of comparable sales.**

Everything you need to judge the deal is here: the valuation range, the Comp Band, the repair
budget and target margin the numbers were built on — yours when you sent them, this service's
own figures when you did not — the maximum allowable offer with its `clamped` flag, the
cost stack, and the `subject_property` block showing which house the numbers actually ran on.
The one thing that is not here is `comps` — the individual sales behind the band.

**Where the comparable sales are.** Score the property on its own with `POST /v1/predict` and
the full `comps` list comes back. Ask by address: a Scored Call identifies a property by
address only, and `property_id` is not one of the members it accepts. The `property_id` in the
batch entry's `subject_property` block is still worth keeping — it tells you which record the
batch scored, and it is the identifier [the History Call](/history) takes when the recorded
transaction story is what you want.

**Why it works this way.** A batch of ten properties would otherwise return ten comp lists, a
payload written for nobody. The evidence that supports each number is still in the answer:
the Comp Band is the range the sales imply, `effective_sample_size` says how many qualified,
and `as_is_similar_band` is the price-per-square-foot range taken over them. The workflow the
endpoint is built for is *triage on the batch, then look closely at the two that pencil* —
and looking closely is a single call.

## The vocabulary is the vocabulary

A batch entry names its members exactly as a single Scored Call names them — the headline
is **`max_allowable_offer`**, and every other member means what it means on
`POST /v1/predict`. There is one scoring vocabulary and one version of it, on every path
that returns a price; see the [Versioning page](/versioning).

If you are reading `max_allowable_offer` for the first time, [reading a
score](/reading-a-score) explains what the figure is — a ceiling, not a prediction — and
what `clamped` is telling you when it reads `true`.

## The request

A `properties` array, each entry exactly the body `POST /v1/predict` accepts:

```json
{
  "properties": [
    { "address": "742 Evergreen Terrace, Springfield, IL 62701" },
    { "address": "1026 N Beckley Ave, Dallas, TX 75203", "condition": "major_rehab" }
  ]
}
```

The envelope accepts **no other members**. Duplicate addresses are allowed, and each one is
charged — two identical entries are two work items.

**The batch size limit is published in the [API reference](/api)** as `maxItems` on the
`properties` array, and it is the limit the API actually enforces.

## What it costs, and when Credits come back

Two halves, and both matter:

- **Charged per property submitted.** A batch of ten costs ten Credits.
- **Refunded per property that fails** for a reason the refund policy covers — one Credit
  back per such property, as its own Refund on your Ledger.

### The whole-batch rules

**A batch is charged in full before any property is scored.** The cost is known and paid up
front, as a single Spend.

**A batch that cannot be charged in full is rejected in full.** If your Balance cannot cover
every property in it, nothing is charged, nothing is split, and the request can be retried
unchanged after a top-up. Batches are never partially accepted.

**A batch over the documented limit is rejected before any Spend**, with a `413` carrying
honest `limit` and `submitted` counts, so you can see exactly how far over you were.

### The per-property rules

**A property that fails for a covered reason does not stop the ones after it.** Its entry
carries the failure, its share of the Credits comes back as its own Refund, and every
property after it is still attempted.

**A failed property's entry is the same Problem Detail a single call would have returned for
it** — the same `type`, the same wording. An integration branching on `type` needs no second
contract. The response is still `200`; the entry itself says what happened.

**The covered reasons are the same as for a single Scored Call, and there are five:** the
address matched more than one property; the address is outside the covered area; the address
could not be scored; an upstream service could not complete the call; or an upstream service
did not answer in time. Each of the five is on the [errors page](/errors) with its own `type`,
and batch adds none of its own.

**A refusal outside those five stops the batch.** If a property fails in a way none of the
five covers, the batch stops there: the properties after it are not attempted, no `results`
array comes back, and the whole request answers
[`503 spend-indeterminate`](/errors/spend-indeterminate) instead. Every Credit recorded for
the batch is returned to the Account as a Refund — including for the properties that had
already scored, whose answers are not returned — and the request is retryable.

**You are charged for every property that produced an answer, and for nothing else.**

## Reading the response

```json
{
  "results": [
    { "model_estimate_available": true, "subject_property": { }, "max_allowable_offer": { } },
    { "type": "https://docs.investorliftdata.com/errors/unscorable-address", "status": 422 }
  ]
}
```

One entry per submitted property, in submission order, **none omitted and none reordered**.
Entry `i` answers submitted property `i` — the position is how you identify which property an
entry belongs to.

**Telling the two apart:** only an error entry carries `status` and `type`. A scored entry
carries neither.

**The Trace ID rides the `trace-id` response header**, not the body, and every entry in one
batch shares the one Trace ID of the call that produced it. Quote it if you contact support
at `john@investorliftdata.com` about a batch.

## Retries

Send an `Idempotency-Key` header to make a retry safe. Keys behave exactly as they do on
`POST /v1/predict` — see [idempotency](/idempotency). Sending the same body to the
single endpoint and the batch endpoint under one key is a conflict, never a replay.
