Operate · AVM
Integration Guide
This guide covers production architecture, client behaviour, release handling, monitoring and go-live controls.
Recommended Architecture
browser/mobile/user system
|
v
customer backend or integration service
|
| Authorization: Bearer <Valtaic key>
v
https://api.valtaic.io
|
v
Valtaic Property Valuation APINever call Valtaic directly from untrusted browser code with a live API key.
Client Responsibilities
A production client should:
- validate required fields and enums before transmission;
- create useful optional
request_referencevalues and retain server IDs; - supply UPRN wherever possible;
- store request and response with
release_id; - use connection pooling and HTTP keep-alive;
- set bounded connect/read timeouts;
- interpret row status independently of HTTP status;
- honour quotas, limits and
Retry-After; - avoid logging credentials or unnecessary personal address data;
- route manual referrals and permanent failures to operations.
Python Example
import os
import time
import random
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import requests
BASE_URL = "https://api.valtaic.io"
API_KEY = os.environ["VALTAIC_API_KEY"]
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
})
def retry_delay(header: str) -> float:
try:
return max(0.0, float(header))
except ValueError:
try:
return max(0.0, (
parsedate_to_datetime(header) - datetime.now(timezone.utc)
).total_seconds())
except (TypeError, ValueError, OverflowError):
return 0.0
def value_property(payload: dict) -> dict:
for attempt in range(4):
response = session.post(
f"{BASE_URL}/v3/simple-value",
json=payload,
timeout=(3.0, 30.0),
)
if response.status_code < 400:
body = response.json()
if body["status"] != "valued":
raise RuntimeError(f"non-valued result: {body}")
return body
if response.status_code not in {429, 500, 503}:
response.raise_for_status()
try:
error = response.json()
except ValueError:
error = {}
detail = error.get("detail", {}) if isinstance(error, dict) else {}
code = detail.get("code") if isinstance(detail, dict) else detail
if code == "monthly_valuation_quota_exceeded":
response.raise_for_status()
records = [error, detail]
if isinstance(error, dict) and isinstance(error.get("errors"), list):
records.extend(error["errors"])
if any(isinstance(item, dict) and item.get("retryable") is False
for item in records):
response.raise_for_status()
retry_after = retry_delay(response.headers.get("Retry-After", "0"))
if attempt == 3 or retry_after > 20:
# Return control rather than shortening a server-directed wait.
response.raise_for_status()
delay = max(retry_after, random.uniform(0, min(20, 0.5 * 2**attempt)))
time.sleep(delay)
raise RuntimeError("Valtaic request failed after bounded retries")This example retries selected HTTP errors, not connection timeouts, permanent quota failures or errors explicitly marked non-retryable. Reconcile uncertain synchronous outcomes before repeating them; the same request reference does not prevent another valuation or charge.
TypeScript Server Example
const baseUrl = "https://api.valtaic.io";
const apiKey = process.env.VALTAIC_API_KEY;
if (!apiKey) throw new Error("VALTAIC_API_KEY is missing");
export async function valueProperty(payload: unknown) {
const response = await fetch(`${baseUrl}/v3/simple-value`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(30_000),
});
const body = await response.json();
if (!response.ok) {
throw new Error(`Valtaic ${response.status}: ${JSON.stringify(body)}`);
}
if (body.status !== "valued") {
return { kind: body.status, result: body };
}
return { kind: "valued", result: body };
}This minimal example omits retries. Implement them only for the transient statuses documented in Errors and retries.
Request References and Server IDs
request_reference is an optional customer-defined correlation value. It is
neither the authoritative valuation identifier nor an idempotency key for
synchronous requests. execution_id identifies the API execution and
valuation_id identifies each property valuation; retain both server-generated
identifiers.
Recommended pattern:
<system>-<entity>-<valuation-date>-<attempt-version>Do not place secrets or unnecessary personal data in it.
Release Discovery
At application startup and periodically thereafter, call
GET /v3/releases/current. Use it to discover:
- active release ID;
- model effective date;
- allowed valuation-date window;
- supported countries/property types;
- synchronous and asynchronous limits;
- published endpoints.
Cache for minutes, not indefinitely. Never hard-code a rolling date boundary.
Valuation Release Changes
When the active release changes:
- record the new release ID and effective date;
- keep old valuations tied to their original release;
- do not rewrite history silently;
- rerun a customer acceptance sample if the integration is material;
- distinguish expected valuation movement from API contract errors;
- use V3 contract tests to detect incompatible response changes.
The same input can have a different point estimate under a new release. That is expected. Separately, use the current API schema and maintain contract tests; a valuation release ID is not an identifier for every API software deployment. Retain the original response instead of relying on a later replay to reproduce it. See Versioning and Releases.
Batch Design
For synchronous batches:
- stay below the lower of release and plan row limits;
- account for JSON body size, not only row count;
- use a useful
request_referenceper row where your own reconciliation needs one; - inspect every row status;
- use simple batch for high-throughput value refreshes;
- avoid requesting expensive evidence unless needed.
For larger work, use jobs and idempotency when the job endpoint appears in the active release metadata. Do not emulate a job by holding a single synchronous connection open indefinitely. See Limits and Safeguards and Latency and Throughput when setting batch sizes, timeouts and concurrency.
Address Resolution
Every existing-property request supplies building/name, street and postcode. A supplied UPRN is verified against that address; an omitted UPRN is resolved. If you depend on address resolution:
- retain the submitted address for reconciliation;
- treat fuzzy/non-exact errors as data-quality work, not transient outages;
- do not change an address automatically until it happens to match;
- allow users or operations to select the correct registered unit;
- cache resolved UPRNs in your own authoritative property record where lawful.
Stored Data in Customer Interfaces
If your product lets users choose whether Valtaic can fill blank attributes:
- default to
use_stored_data=false; enable it deliberately only when your key has the required scope; - clearly state that explicit values override stored values;
- show fields returned in
stored_data_used; - show ignored inputs when they explain why a field had no effect;
- do not imply that stored data is guaranteed complete.
Security and Privacy
- Use TLS only.
- Keep API keys in managed secrets.
- Apply least-privilege scopes per integration.
- Rotate keys without downtime.
- Avoid storing more address data than the use case requires.
- Encrypt requests/results at rest in your systems.
- Control who can view comparables and property history.
- Log IDs and outcome codes rather than full payloads where possible.
- Define retention for valuation requests and results.
Observability
Capture:
endpoint
request_reference
execution_id
valuation_id
X-Valtaic-Request-Id
release_id
HTTP status
row status
latency
retry count
batch summary
job_idMonitor:
- latency percentiles by endpoint and batch size;
429,500,503rates;- identity failure rate;
- manual referral rate;
- valued-row rate;
- quota consumption;
- asynchronous completion time and failure rate;
- release changes.
Use the service-level terms in your agreement when setting latency and throughput expectations.
Failure Routing
| Result | Product action |
|---|---|
valued | Display and store the value, range, confidence and release ID. |
referred | Offer manual review workflow. |
rejected | Correct the structural or identity error in errors[]. |
valuation_failed, retryable | Retry under policy, then escalate. |
valuation_failed, permanent | Correct data or escalate. |
Go-Live Checklist
- Production hostname and TLS certificate are confirmed.
- Required endpoints appear in
/v3/releases/current. - Live key is stored server-side and has least-privilege scopes.
- Plan row, monthly unit, rate and concurrency limits are understood.
- Valuation dates use the discovered rolling window.
- Contract-invalid requests are rejected in test.
- Supplied-UPRN verification and UPRN-resolution paths are both tested.
- Stored-data true/false and explicit precedence are tested.
- Freehold/leasehold and simple/detailed lease paths are tested.
- Flat, house, bungalow and development conditions are tested.
- Batch partial outcomes are reconciled correctly.
- Job idempotency and polling are tested if used.
- Manual referrals enter a human workflow.
- Retry logic excludes permanent errors.
- Request references, execution IDs, valuation IDs, release IDs and edge request IDs are logged.
- Keys and address payloads are redacted appropriately.
- Customer support has the error and escalation fields it needs.
Contract Testing
Maintain fixtures for:
- one freehold house;
- one simple-mode leasehold;
- one detailed-mode leasehold;
- one flat with inapplicable fields;
- one address-resolved property;
- one stored-data-disabled property;
- one development property;
- one partial batch;
- one asynchronous job;
- one manual referral.
Contract tests should verify schema, status and invariant behaviour rather than requiring exact valuation equality across monthly releases.