# CleanedWeb integration guide for AI agents

This guide describes the current public contract. The API base URL is
`https://app.cleanedweb.com`.

## 1. Discover before acting

Fetch the uncached runtime manifest before composing a request:

```sh
curl --fail-with-body --silent --show-error \
  https://app.cleanedweb.com/v1/capabilities
```

Treat `available_markets` as runtime readiness. A market mentioned on a marketing
page or in static coverage is not necessarily ready or entitled. If capabilities
returns `503`, availability is unknown; do not replace it with an empty market list.

Load the authoritative operation contract and referenced schemas:

- `https://cleanedweb.com/openapi.json`
- `https://cleanedweb.com/schemas/property-profile-v2.json`
- `https://cleanedweb.com/schemas/property-event-v1.json`
- `https://cleanedweb.com/schemas/error-v1.json`
- `https://cleanedweb.com/schemas/property-profile-field-catalog-v2.json`
- `https://cleanedweb.com/arazzo.json`
- `https://cleanedweb.com/asyncapi.json`
- `https://cleanedweb.com/changelog.json`

These static artifacts are intentionally hosted on `cleanedweb.com` and do not
require a portal session. Their OpenAPI operations still target the API base at
`https://app.cleanedweb.com`.

Reject unknown major contract or schema versions instead of guessing.
Use the field catalog for semantics and units; JSON Schema remains authoritative for
structure. Use Arazzo for deterministic operation order and the workflow guide for
human decisions and stop conditions.

## 2. Authenticate without exposing credentials to the model

`GET /v1/capabilities` is public at the API base. OpenAPI, schemas, and examples are
public at `cleanedweb.com`. Browser property operations use the production workspace
session cookie named `__Host-cw_session`.
Machine clients may use a workspace Bearer API key only when capabilities returns
`machine_access.enabled: true`.

A human signs in at `https://app.cleanedweb.com`. The HTTP client or trusted tool
adapter owns the resulting cookie. Never place the cookie in a prompt, model-visible
tool result, log, URL, or generated code. For protected `POST` requests, send
`Origin: https://app.cleanedweb.com`; the server enforces same-origin submission.

If the session is missing or expired, return an authentication-required action to
the user. Do not automate magic-link inbox access unless the user has separately
authorized that mailbox workflow.

API-key secrets use the `cw_<environment>_...` form, are shown once, and must stay
outside model context. They have only the `properties:read` scope. A machine search
must include a bounded `limit`, `max_units`, and an `Idempotency-Key` header.

## 3. Confirm workspace markets

With the session cookie, call:

```sh
curl --fail-with-body --silent --show-error \
  --cookie "__Host-cw_session=SESSION_VALUE" \
  https://app.cleanedweb.com/v1/markets
```

This endpoint returns only markets that are both enabled for the workspace and
ready in the maintained serving projection. Use `data_updated_at` and
`available_records` as snapshot metadata, not as a promise of source-real-time data.

## 4. Resolve locations before searching

For user-entered location text, request canonical candidates:

```sh
curl --fail-with-body --silent --show-error \
  --cookie "__Host-cw_session=SESSION_VALUE" \
  "https://app.cleanedweb.com/v1/properties/locations?market=LU&q=Belair&limit=8"
```

Present ambiguous candidates to the user or select one only when the user's intent
is unambiguous. Send both `location` and the selected `location_place_id` in search.
The default candidate limit is 8 and the maximum is 20.

For a question that only asks "how many", call the exact aggregate instead of
searching and discarding rich records:

```sh
curl --fail-with-body --silent --show-error \
  --request POST \
  --cookie "__Host-cw_session=SESSION_VALUE" \
  --header "Origin: https://app.cleanedweb.com" \
  --header "Content-Type: application/json" \
  --data '{"market":"ES","response_schema":"property-profile-v2","deal_type":["buy"],"location":"Madrid","max_price":250000,"min_bedrooms":3}' \
  https://app.cleanedweb.com/v1/properties/count
```

`min_bedrooms` is inclusive, so "more than two" is `3`. The response contains
`total_matched`, whether the projection-bound exact count was cached, the
resolved query, and the snapshot identity; it contains no property records and
does not consume delivery Units.

## 5. Search the maintained snapshot

Use the published example as a starting point:
`https://cleanedweb.com/examples/search-request.json`.

```sh
curl --fail-with-body --silent --show-error \
  --request POST \
  --cookie "__Host-cw_session=SESSION_VALUE" \
  --header "Origin: https://app.cleanedweb.com" \
  --header "Content-Type: application/json" \
  --data '{"market":"LU","response_schema":"property-profile-v2","deal_type":["buy"],"limit":100}' \
  https://app.cleanedweb.com/v1/properties/search
```

Bounded searches return `has_more` and an opaque signed `next_cursor`. Send the
cursor with the unchanged query and positive `limit` to read the next page. If
the projection advances, `snapshot_changed` requires a restart from page one.
Omit `limit` to request every match. Before committing results, require all of
the following:

1. `returned === results.length`.
2. Every result has `schema_version === "property-profile-v2"`.
3. Every `property_entity_id` is present and unique within the response.
4. `snapshot.dataUpdatedAt` satisfies the workflow's freshness policy.
5. `has_more === false` after aggregating cursor pages, or use one complete
   unbounded response, when a complete snapshot is required.

Use `property_entity_id` as the canonical upsert key. Retain `sources` as
provenance; do not turn a source listing ID into the canonical property identity.

Machine clients cannot omit `limit`. They must also add `max_units` to the JSON body
and send a unique `Idempotency-Key`. A successful response includes `usage` plus
`X-CleanedWeb-Request-Id`, `X-CleanedWeb-Units`, and
`X-CleanedWeb-Unit-Balance` headers.

## 6. Retrieve one property or its changes

The authenticated OpenAPI contract publishes:

- `GET /v1/properties/{property_entity_id}` for the current canonical property.
- `GET /v1/properties/{property_entity_id}/changes?limit=50` for newest-first,
  finalized observed changes. The change limit must be between 1 and 200.

Suspected transitions and incomplete observer runs are excluded from the finalized
change stream. Notification delivery is not the system of record; re-read the
property endpoint when a workflow needs current state.

## 7. Metering and receipts

Read `unit_costs` from `GET /v1/capabilities`. An empty object means no operation
cost is published through this runtime contract; do not invent a cost from pricing
copy, an HTTP status, or a webhook field. Workspace receipts or the Unit ledger are
the billing source of truth when they are available.

## 8. Failure policy

Parse the `error-v1` envelope for operations covered by OpenAPI. Do not retry
authentication, entitlement, not-found, or validation failures unchanged. Retry a
`503` with bounded exponential backoff and jitter. See
`https://cleanedweb.com/docs/errors.md` for the exact boundary and recovery table.

## 9. Deterministic acceptance policy

The reference acceptance policy distinguishes successful and empty searches from
unsafe responses. It stops on unsupported markets, unknown contract or property
schema versions, duplicate canonical IDs, response-count mismatches, stale
snapshots, and unexpected truncation. It requests user input for ambiguous
locations, retries bounded transient failures and rate limits, reuses matching
idempotent receipts, and acknowledges already-processed webhook IDs without running
their business effect twice.

Unit-budget, durable idempotency-receipt, and shared rate-limit cases apply only when
runtime `machine_access.enabled` is true and `unit_costs` is non-empty. Otherwise,
do not invent an estimate or apply the machine budget path.

## Data boundary

CleanedWeb returns maintained delivery-safe canonical records, not raw acquisition
artifacts. Optional values can be absent. Do not infer a missing value, legal right,
freshness level, or real-world event that the response does not state.
