Agent control loop
Discover, decide, then act.
Stop whenever runtime capability, authority, request bounds, or delivery acceptance is unresolved.
- 01DiscoverRead capabilities
- 02GateMachine access enabled?
- 03AuthorizeTrusted client owns key
- 04BoundMarket, limit, Units
- 05AcceptValidate and persist
| Runtime state | Decision | Next action |
|---|---|---|
| Capability absent | Stop | Do not infer an operation |
| Location ambiguous | Pause | Ask for a candidate choice |
| Bounded response accepted | Continue | Persist response and receipt |
| Schema or count mismatch | Reject | Keep evidence and escalate |
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:
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.jsonhttps://cleanedweb.com/schemas/property-profile-v2.jsonhttps://cleanedweb.com/schemas/property-event-v1.jsonhttps://cleanedweb.com/schemas/error-v1.jsonhttps://cleanedweb.com/schemas/property-profile-field-catalog-v2.jsonhttps://cleanedweb.com/arazzo.jsonhttps://cleanedweb.com/asyncapi.jsonhttps://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:
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:
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:
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.
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:
returned === results.length.- Every result has
schema_version === "property-profile-v2". - Every
property_entity_idis present and unique within the response. snapshot.dataUpdatedAtsatisfies the workflow's freshness policy.has_more === falseafter 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=50for 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.
Standards and further reading