Skip to main content

Contents Estimation integration

Contents Estimation is the clean-stack rebuild of Adjust Square (the AI contents estimation / claims platform for public adjusters). ContentsVision can push a finished claim inventory into it, which creates the claim and its items there and starts pricing. This is handled by the send endpoint (POST /api/claims/{id}/submit-to-contents-estimation) and a thin API client in backend/app/services/contents_estimation.py.

It is a separate, independent action from the Adjust Square submission: sending to Contents Estimation neither reads nor changes the per-item submitted locks or the Adjust Square claim state, so the same claim can be sent to either platform (or both) without one interfering with the other.

What sending does

Sending gathers every active (non-archived) item in the claim across all its rooms, builds one intake payload (claim fields + rooms + items, with photos as URLs), and posts it to Contents Estimation's POST /intake/contentsvision. There is no room selection, no pricing-profile step, and no separate "start processing" call: intake enqueues Contents Estimation's own pricing pipeline automatically.

Payment (charged at send)

When prepaid billing is on, the send captures payment in ContentsVision, mirroring the upload gate: the team is charged cost_per_contents_estimation_item credits (default 1) per active item, once per claim, keyed ce-send:{claim_id}. The charge runs before the intake call (billing.charge_credits) and doubles as the affordability gate: a flat refusal returns HTTP 402 and nothing is sent. If the intake then fails, the charge is refunded best-effort (billing.refund_credits). A re-send (a claim already linked to a Contents Estimation claim) is free, and Contents Estimation does not re-bill the shared credit pool. The confirm modal prices the send up front via GET /api/claims/{id}/contents-estimation-quote; the submit response returns credits_charged. With billing off (the default), sending charges nothing.

Which teams can send, and the button

The claim page's send buttons are gated by two per-team feature flags delivered on /api/auth/me (features.adjust_square, features.contents_estimation), toggled from the admin Teams tab. Adjust Square is off by default (opt-in per team); Contents Estimation is on by default. With one integration enabled the page shows a single Send button; with both, a Send to dropdown (SendToMenu). After a send, the status badge deep-links into the assignment (Adjust Square {app_url}/lkq/{claim_key}, Contents Estimation {app_url}/claims/{claim_uuid} — see ADJUST_SQUARE_APP_URL / CONTENTS_ESTIMATION_APP_URL).

Once a claim has been sent to Contents Estimation, its Send action is replaced by a View in Contents Estimation button linking to {app_url}/claims/{claim_uuid} (requires the stored contents_estimation_claim_id and a configured app URL). Contents Estimation then drops out of that claim's send options — so with both integrations on, the page shows the Adjust Square Send for Estimation button next to View in Contents Estimation rather than the dropdown.

Idempotent, one-shot

The push is idempotent on the claim's id, which is sent as the intake's external_reference. Contents Estimation derives a business claim_key of cv-<external_reference> and dedupes on it: a retry (or a later re-send) returns the same claim with created: false instead of creating a duplicate. A replay does not add new items on that side, so this is a whole-inventory, one-shot hand-off rather than the incremental re-submission the Adjust Square path supports.

Because a retry is always safe, the send runs synchronously (one fast call) and stores the returned claim_key / claim_id on the claim.

The Contents Estimation endpoint used

ContentsVision client functionContents Estimation endpointUsed for
submit_intakePOST /intake/contentsvisionCreate a claim + rooms + items from a whole inventory, and start pricing.

The response (IntakeResult) carries claim_id (the remote claim uuid), claim_key (cv-<claim id>), status (intake), item_count, and created (true on first ingest, false on an idempotent replay).

Field mapping

ContentsVision stores human-readable claim values and maps them to Contents Estimation's lowercase enums in claims.py before sending. Contents Estimation validates these strictly, so an unmapped value is omitted, never sent raw:

ContentsVision valueIntake fieldMapping
loss_type (e.g. "Fire", "Flood / Water Damage")peril_PERIL_MAP then guarded against _CE_PERILS; unmapped is omitted.
coverage_type ("ACV" / "RCV")coverage_type_CE_COVERAGE_MAP (ACV → acv, RCV → rcv); other values omitted.
claim_type ("Residential" / "Commercial")claim_type_CE_CLAIM_TYPE_MAP (lowercased); other values omitted.
claim.idexternal_referenceThe idempotency key.
claim_numbermetadata.contentsvision_claim_numberKept in metadata (the derived claim_key is a uuid, so the human claim number is preserved for reference).
zip_codeloss_address.zip
insured_namepolicyholder.name

Each item maps its title, quantity, brand, sku, condition, age, notes (as description), room, and estimated price. external_id carries the ContentsVision item id for traceability, and model/serial/damage/item-number extras (with no first-class intake field) are preserved under the item's details.

Photos travel as URLs

The intake takes photos as URLs, not bytes (unlike Adjust Square's per-photo byte uploads). Each item's photos are turned into absolute URLs by prefixing the stored /api/photos/... path with PUBLIC_BASE_URL (a public, unauthenticated route). The primary photo (first) also becomes the item's image_url. When PUBLIC_BASE_URL is empty (e.g. local dev, where localhost is not reachable from the other side), photos are omitted and the inventory still sends.

Authentication: per-team, from SSO

Auth mirrors the Adjust Square integration: a per-team machine-to-machine API key (a <key_id>.<secret> token carrying the intake:write scope, bound to one team on the Contents Estimation side), sent in the X-API-Key header. It is captured from the AdjustSquare SSO JWT and stored on Team.contents_estimation_key, then resolved per request from the signed-in user's team (_resolve_ce_key). So each ContentsVision team's claims land under its own Contents Estimation tenant, not a single shared one.

  • Instance config is only CONTENTS_ESTIMATION_BASE_URL (one host per deployment) — that is the "configured" gate, exactly like ADJUST_SQUARE_BASE_URL.
  • CONTENTS_ESTIMATION_API_KEY is an optional global fallback, used only for a request whose team has no per-team key yet (the SSO issuer hasn't shipped the claim, or a non-SSO / single-tenant setup). It mirrors ADJUST_SQUARE_BEARER_TOKEN and is not required.
  • A team with neither a per-team key nor a fallback gets a clear 409 at submit; if the instance has no base URL, the 409 fires earlier.
Contents Estimation side (SSO claim contract)

For the per-team key to flow automatically, the AdjustSquare SSO issuer must include a current_team_ce_intake_key claim in the JWT (the team's intake:write key), and Contents Estimation must provision one such key per team. Until that ships, set CONTENTS_ESTIMATION_API_KEY as the fallback so the integration works instance-wide. See the configuration reference.

Training claims

Sending a training claim is a dry run: it records an obviously-fake claim key (TRAINING-DRY-RUN-...) and never contacts Contents Estimation, so training needs no configured integration and spends nothing (mirroring the Adjust Square training dry run).

Error handling

Every client call converts failures into ContentsEstimationError, which carries the HTTP status and response body. Network failures (timeout, reset, DNS) are caught and reported as "unreachable" rather than bubbling up as an opaque 500.

SituationResult
Contents Estimation not configured (missing base URL or API key)Send endpoint returns 409 before any network call.
The claim has no active itemsSend endpoint returns 400.
Contents Estimation rejects the payload (non-2xx, e.g. 422 on a bad enum)ContentsEstimationError with status + body; the route returns 502.
The external_reference is already used by another tenantThe intake returns 409; surfaced as a 502 on this side.

Where it lives

LayerLocation
API clientbackend/app/services/contents_estimation.py
Send endpoint + payload buildersbackend/app/api/routes/claims.py (submit_claim_to_contents_estimation, _build_contents_estimation_payload, _resolve_ce_key)
Per-team key (from SSO)Team.contents_estimation_key, captured in core/auth_deps.py from the current_team_ce_intake_key JWT claim (core/security.py)
Claim tracking columnsClaim.contents_estimation_claim_key / _claim_id / _submitted_at
Readiness surfaceGET /api/auth/debugcontents_estimation block
Frontendfrontend/src/components/SendToContentsEstimationModal.tsx, wired on the claim detail page
Testsbackend/tests/test_contents_estimation*.py

The authoritative wire contract lives on the Contents Estimation side as Pydantic models (api/app/intake/schemas.py), with a human-readable companion at specs/intake-contentsvision.md.