# Retry safely

import { Card } from "zudoku/ui/Card";
import { CircleAlert, ListChecks } from "zudoku/icons";

Keep the same `Idempotency-Key` when you retry one logical call.

## Choose a key

The header is optional, and absence is never an error.
Without it, every send is a new, charged request.

Keys are scoped to your [account](/definitions), the record that owns your keys and [balance](/definitions), the money available for API calls.
One account cannot replay another account's result.

Use **255 characters or fewer**, **no control characters**, and a value that is **not blank**.
Tabs, carriage returns and line feeds are control characters.

Invalid keys receive [`400 invalid-request`](/errors/invalid-request) before anything is charged.
Keys are never trimmed; surrounding spaces make keys different.

## Before: send the call

Choose one key for this request and keep its body and endpoint with it.
Keep every body byte the same, including spaces and JSON field order.

<CodeTabs syncKey="request-language" hideIcon>

```bash title="cURL"
curl https://api.investorliftdata.com/v1/predict \
  --request POST --max-time 30 \
  --header "Authorization: Bearer $INVESTORLIFT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: property-42-v1" \
  --data '{"address": "1026 N Beckley Ave, Dallas, TX 75203"}'
```

```javascript title="JavaScript"
const response = await fetch("https://api.investorliftdata.com/v1/predict", {
  method: "POST",
  signal: AbortSignal.timeout(30_000),
  headers: {
    Authorization: `Bearer ${process.env.INVESTORLIFT_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "property-42-v1",
  },
  body: '{"address": "1026 N Beckley Ave, Dallas, TX 75203"}',
});
console.log(await response.json());
```

```python title="Python"
import os
import requests

response = requests.post(
    "https://api.investorliftdata.com/v1/predict",
    headers={
        "Authorization": f"Bearer {os.environ['INVESTORLIFT_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": "property-42-v1",
    },
    data='{"address": "1026 N Beckley Ave, Dallas, TX 75203"}',
    timeout=30,
)
print(response.json())
```

</CodeTabs>

## After: retry the same call

If the answer is lost, resend this exact body and endpoint with the same key.
A stored success is replayed without another charge.

<CodeTabs syncKey="request-language" hideIcon>

```bash title="cURL"
curl https://api.investorliftdata.com/v1/predict \
  --request POST --max-time 30 \
  --header "Authorization: Bearer $INVESTORLIFT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: property-42-v1" \
  --data '{"address": "1026 N Beckley Ave, Dallas, TX 75203"}'
```

```javascript title="JavaScript"
const response = await fetch("https://api.investorliftdata.com/v1/predict", {
  method: "POST",
  signal: AbortSignal.timeout(30_000),
  headers: {
    Authorization: `Bearer ${process.env.INVESTORLIFT_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "property-42-v1",
  },
  body: '{"address": "1026 N Beckley Ave, Dallas, TX 75203"}',
});
console.log(await response.json());
```

```python title="Python"
import os
import requests

response = requests.post(
    "https://api.investorliftdata.com/v1/predict",
    headers={
        "Authorization": f"Bearer {os.environ['INVESTORLIFT_API_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": "property-42-v1",
    },
    data='{"address": "1026 N Beckley Ave, Dallas, TX 75203"}',
    timeout=30,
)
print(response.json())
```

</CodeTabs>

<details>
<summary>Show the full response (145 lines)</summary>

```json title="Full response" operation="predict"
{
  "comps": [
    {
      "sqft": 2080,
      "lotsize": 6500,
      "bedrooms": 6,
      "bathrooms": 2,
      "sale_date": "2024-12-03",
      "sale_price": 246641,
      "year_built": 1945,
      "address_zip": "75203",
      "property_id": "00000000000000000000000000000001",
      "address_city": "Dallas",
      "address_state": "TX",
      "property_type": "Single Family",
      "address_street": "523 N Marsalis Ave",
      "distance_miles": 0.4,
      "similarity_rank": 1,
      "price_per_square_foot": 119
    },
    {
      "sqft": 1400,
      "lotsize": 7025,
      "bedrooms": 3,
      "bathrooms": 1,
      "sale_date": "2026-05-08",
      "sale_price": 234999,
      "year_built": 1930,
      "address_zip": "75203",
      "property_id": "00000000000000000000000000000001",
      "address_city": "Dallas",
      "address_state": "TX",
      "property_type": "Single Family",
      "address_street": "102 E 5th St",
      "distance_miles": 0.09,
      "similarity_rank": 2,
      "price_per_square_foot": 168
    },
    {
      "sqft": 1787,
      "lotsize": 6678,
      "bedrooms": 4,
      "bathrooms": 2,
      "sale_date": "2025-03-05",
      "sale_price": 503139,
      "year_built": 1945,
      "address_zip": "75208",
      "property_id": "00000000000000000000000000000001",
      "address_city": "Dallas",
      "address_state": "TX",
      "property_type": "Single Family",
      "address_street": "818 Elsbeth St",
      "distance_miles": 0.24,
      "similarity_rank": 3,
      "price_per_square_foot": 282
    }
  ],
  "comp_band": {
    "low": 388634,
    "high": 942020,
    "median": 571412
  },
  "cost_stack": {
    "loan": 331200,
    "points": 6624,
    "interest": 6533,
    "hold_days": 60,
    "down_payment": 82800,
    "closing_costs": 29562,
    "target_profit": 114282
  },
  "rehab_budget": 0,
  "target_margin": 0.2,
  "valuation_mean": 1016099,
  "subject_property": {
    "facts": {
      "bathrooms": {
        "value": 2,
        "source": "county-record"
      },
      "year_built": {
        "value": 1935,
        "source": "county-record"
      },
      "square_feet": {
        "value": 2047,
        "source": "county-record"
      },
      "property_type": {
        "value": "Single Family",
        "source": "county-record"
      },
      "lot_size_square_feet": {
        "value": 7100,
        "source": "county-record"
      }
    },
    "address": {
      "zip": "75203",
      "city": "Dallas",
      "state": "TX",
      "street": "1026 N Beckley Ave"
    },
    "property_id": "00000000000000000000000000000001",
    "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": []
    }
  },
  "as_is_similar_band": {
    "basis": "all-comparable-sales",
    "statement": "Too few comparable sales passed the size, age and recency filter this service applies to state a similar-sales range, so this price-per-square-foot range covers every comparable sale below that carries a usable sale price and square footage. `qualifying_sales` is how many passed the filter and fell short.",
    "qualifying_sales": 1,
    "price_per_square_foot": {
      "low": 190,
      "high": 460,
      "median": 279
    }
  },
  "rehab_adjusted_arv": 571000,
  "max_allowable_offer": {
    "amount": 414000,
    "clamped": false
  },
  "valuation_quantiles": {
    "0.02": 191787,
    "0.10": 345336,
    "0.20": 476815,
    "0.30": 598093,
    "0.40": 733771,
    "0.50": 878872,
    "0.60": 1110818,
    "0.70": 1417051,
    "0.80": 1988162,
    "0.90": 3615373,
    "0.98": 15505619
  },
  "effective_sample_size": 51.04950714111328,
  "model_estimate_available": true,
  "pricing_model_arv_estimate": 1417000
}
```

</details>

## Reuse a key correctly

The replay window is 24 hours; see the [formal rules](/api#idempotent-requests).
Inside it, the same request and key return the stored response.
After 24 hours, the stored response expires and a repeat is a new, charged request.

Only successful responses are stored.
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.
Retrying after a failed or refunded `502` or `504` is a fresh attempt that spends again.

Only while an earlier success remains stored does a different body or endpoint cause [`422 idempotency-key-conflict`](/errors/idempotency-key-conflict).
Nothing is charged for that refusal; use a new key for the changed request.

An identical request still running receives [`409 duplicate-request-in-flight`](/errors/duplicate-request-in-flight), with no charge for the copy.
Wait for its `Retry-After` interval, then ask again with the same key.

## Check consent and free calls

The Terms-consent check runs before any key lookup, including a replay.
If current Terms need acceptance, [`403 terms-not-accepted`](/errors/terms-not-accepted) refuses the send without a charge.
Accepting them restores the replay while its window remains open.

The free `POST /v1/validate` call ignores `Idempotency-Key` entirely.
See [Test without spending](/production/test-without-spending) for its availability rules.

The API never retries for you.
A [charged call](/definitions), an API request with a price, can fail after it is charged, so you control each new attempt.

## Next steps

<div className="docs-next-steps">

<Card className="docs-next-card"><a href="/production/handle-errors"><CircleAlert className="docs-next-icon" size={18} strokeWidth={1.5} aria-hidden="true" focusable="false" /><strong>Handle errors</strong><span className="docs-next-description">Choose the right action before retrying.</span><span className="docs-next-arrow" aria-hidden="true">→</span></a></Card>

<Card className="docs-next-card"><a href="/production/go-live-checklist"><ListChecks className="docs-next-icon" size={18} strokeWidth={1.5} aria-hidden="true" focusable="false" /><strong>Go-live checklist</strong><span className="docs-next-description">Check the rest of your integration.</span><span className="docs-next-arrow" aria-hidden="true">→</span></a></Card>

</div>
