---
name: zoodealio-api
description: "Build apps, scripts and integrations against the Zoodealio public REST API (properties, photos, documents, offers, AI comping, transactions, team, webhooks). Use when writing or debugging code that calls a Zoodealio /openapi endpoint or handles a Zoodealio API key."
---

# Building on the Zoodealio API

Base URL: `https://api.zoodealio.ai/openapi`

The API does what an agent can do in the Zoodealio app, and nothing more: submit and edit properties, add photos and documents, accept an estimate (which starts AI comping), request an offer's details and accept an offer. Offers are made by Zoodealio; there is no endpoint to create, edit or counter one.

## Look it up, never guess

Field names and paths are not what you would guess (`fName` not `firstName`, `homeownerId`, `Team/members`). Before writing a call, read the exact endpoint:

- Docs MCP server (no key): `https://zoodealio.ai/developers/mcp`. Tools: `search_docs`, `get_section`, `list_endpoints`, `get_skill`. `get_section quickstart` is the whole journey.
- Full reference as one Markdown file: https://zoodealio.ai/developers/llms-full.txt
- Human docs: https://zoodealio.ai/developers

## Authentication

- Every request sends the key in the `ApiKey` header (or `Authorization: Bearer <key>`). Keys look like `zoo_…`.
- Read the key from an environment variable such as `ZOODEALIO_API_KEY`. Never hard-code it, commit it, log it, or put it in client-side (browser) code: the key acts as the user, with their role and access. Call the API from a server.
- A key is personal. There is no separate app identity, so what a script can see is exactly what its owner can see in the app.

## The journey, in order

1. `POST /openapi/Webhooks` once: subscribe to `transaction.stage_changed`, `offer.created`, `property.files_imported`, `comping.completed`, `comping.failed`, `offer.details_shared`, `offer.accepted` (see the `zoodealio-webhooks` skill).
2. `POST /openapi/Properties` with `address`, `attributes` and `homeowner` (`email`, `fName`, `lName`, `phone`). Photos and documents ride along as URLs you host (`photos: [{ url, label }]`, `documents: [{ url, fileName }]`, up to 200); Zoodealio imports them in the background. Returns `{ id, homeownerId, importId, importStatusUrl }`; the property is priced immediately (`offer.created`) and `property.files_imported` fires when the files are in.
3. `GET /openapi/Offers?propertyId={id}`: the estimate is `kind: "Estimated"` with only `estimatedRange: { low, high }`.
4. `POST /openapi/Offers/{estimateId}/accept`: starts AI comping. Optionally send JSON `attributes` (the homeowner-confirmed details); an empty body skips it. Photos are not accepted here: import them first (`POST /openapi/Properties/{id}/photos`) and accept after `property.files_imported`, so comping uses them.
5. Wait for `comping.completed` (or `comping.failed`). Read offers again: each `kind: "Actual"` offer has its headline `amount`; terms are `null`.
6. `POST /openapi/Offers/{offerId}/request-details`, then wait for `offer.details_shared` and read the offer for its terms.
7. `POST /openapi/Offers/{offerId}/accept` with no body selects the offer (stage `OfferSelected`).

## Rules that cause most bugs

1. Steps are ordered. Taking one out of order is a 409; read the transaction's `stage` (`GET /openapi/Transactions/active?propertyId=…`) and take the step it is waiting for.
2. `null` amounts and terms mean "not disclosed yet", never zero. Estimates never show more than `estimatedRange`.
3. Ids go in the path: `PUT /openapi/Properties/{id}`. An id in the body against the collection path is a 405.
4. Errors are `application/problem+json`: 400 validation, 401 bad key, 403 role cannot do it, 404 not found or outside the caller's scope, 409 conflict, 413 upload too large. Surface `title` and `detail`.
5. 429 carries `Retry-After` (seconds). Wait that long; do not retry sooner or in a tight loop.
6. Send photos and documents as URLs you host (signed links are fine if they outlive the import), never as base64: up to 200 files per import, photos (JPEG, PNG, WebP) up to 10 MB, documents up to 25 MB. Imports run in the background: the call returns 202 with `importId`, and `property.files_imported` (or GET the `statusUrl`) lists the saved ids and `failed[]` with a reason per file; resend what failed. Multipart upload of local files also works but carries only about 4 MB per request; let the HTTP client set its `Content-Type` boundary.
7. Inviting a team member (`POST /openapi/Team/members`) returns an INVITE id; the person appears in `GET /openapi/Team/members` only after accepting it. A member id is a property's `agentId`.
8. Send `If-Match` with the ETag from a GET when a stale write must be rejected.

Prefer webhooks over polling. When you must poll, poll no more than once every 30 seconds.

## Before calling it done

Exercise the code against a real key on a test workspace, check each non-2xx branch is handled, and confirm no key is printed in logs or committed.
