# Public API changelog

## 1.8.0 — 2026-10-14

- `GET /reporting/overview` returns the Intelligence Overview scorecards for `account_ids` and a `start`/`end` window of up to 32 days, as one object: active units and prospects, days on market for leased and active units, inbound calls answered, median text response time with and without the AI, median hours to each funnel stage, application processing time and AI coverage.
- `GET /reporting/conversations` returns the Command Center inbox: one row per phone number and account with activity after `since` (at most 32 days ago), newest first, paged with `limit` (up to 500) and `page_number`. It filters by `statuses`, `unanswered_only`, `agent_user_ids`, `unit_ids` and `multifamily_property_ids`. `statuses` is repeated rather than comma-separated, because statuses contain commas.

## 1.7.1 — 2026-10-14

- Endpoints that require an API key now answer a revoked key with `401` `{ "error": "This API key has been revoked", "code": "api_key_revoked" }`, where they could return `500` `internal_error` before. The MCP server rejects it with `401` and `error="invalid_token"`. Create a new key in the developer portal.

## 1.7.0 — 2026-09-24

`GET /rental_applications/{id}/income` now reports **both** ways a multi-paystub figure can be measured, and which one it used.

- `sources.documents.estimates`, `IncomeDocumentItem.estimates` and `IncomeDocumentBreakdownSource.estimates` — `{ average, ytd }` at all three levels: the whole document total, one uploaded file, and one employer. `average` is the mean of the per-result figures and is what `computed_monthly_income` / `monthly_income` carry. `ytd` is the year-to-date measurement — the newest paystub's `gross_pay_ytd` over the months elapsed — or null when there is nothing to compute from.
- `estimator` still names the one that was used, and is now always `average`. It previously chose `ytd` for a multi-paystub source whose year-to-date figure looked plausible.
- Where several sources are combined — the `sources.documents` total, or one file covering two employers — `ytd` is the SUM of their estimates, and null unless every one of them has an estimate. A partial sum would understate. In practice it is often null, because a payroll register frequently prints no year-to-date column at all.
- `estimates.ytd` is **raw**: it is not filtered by the plausibility check that used to gate it, and it divides by the months since Jan 1 unless the applicant's self-reported hire date matches the employer. For a mid-year hire who gave no start date it reads implausibly low. Treat it as a second opinion on the same stubs, not an alternative figure to act on.
- Why the selection changed: the check that gated `ytd` had a lower bound but no upper one, so an applicant whose income fell during the year was overstated — full-time for half a year then cut to a third reads about 2.3× their current earnings — and nothing caught it.
- Additive: `estimator` keeps its enum, existing keys and values are unchanged. Figures for existing multi-paystub documents change with this release, in both directions. One whose year-to-date estimate sat below the mean of its stubs moves up; one above it moves down, and that second group is the falling-income case this was made for.

## 1.6.0 — 2026-09-22

- `GET /rental_applications/{id}/income` describes a document that bundles several paystubs or bank statements. A single uploaded PDF can hold more than one, and `computed_monthly_income` has always covered all of them while `details` described only the most recent — leaving the figure impossible to reconcile from the response.
  - `IncomeDocumentItem.period_count` — results in the file; `1` for an ordinary upload.
  - `IncomeDocumentItem.details.periods[]` — every result, newest first, present only when `period_count` > 1. `periods[0]` is the result the top-level `details` fields describe.
  - `IncomeDocumentItem.estimator` — `ytd` or `average`, how `computed_monthly_income` was measured. A bundle measured as `ytd` cannot be derived from the per-period figures.
  - `IncomeDocumentBreakdownSource.period_count` and `.estimator` — the same two on the rollup, where `document_count` continues to count files.
- All fields are additive; existing keys and values are unchanged for single-document uploads.
- `submitted_monthly_income` documents that it is scoped to one application: summing it across a group can double-count a document two applicants both uploaded.

## 1.5.0 — 2026-09-22

The `/reporting/*` endpoints accept nine more filters, all comma-separated lists. Every one is optional, so existing calls are unaffected. Each endpoint documents the ones it supports.

- `unit_ids` — unit IDs. Unioned with `multifamily_property_ids`, so passing both widens the result.
- `zip_codes` — ZIP code of the unit's address.
- `owner_ids` — property-owner IDs. A unit with several owners matches if any is listed.
- `floorplan_ids` — floorplan IDs.
- `agent_user_ids` — agent user IDs.
- `prospect_sources` — prospect sources, matched without regard to case or surrounding spaces.
- `showing_methods` — one of `Accompanied`, `Remote Guided`, `Remote Guided with Gated Access`, `Self Guided`, `Accompanied OR Self Guided`.
- `bedrooms`, `bathrooms` — an exact count (`2`, `1.5`) or an open-ended floor (`5+`). Both forms can be mixed.
- Omitting a filter returns everything. Every report takes all nine except `/reporting/units`, which has no prospect of its own and so takes neither `prospect_sources` nor `agent_user_ids`. An endpoint returns `400` for a filter it does not support.
- `GET /reporting/property-performance` is new: the Intelligence dashboard's Unit Performance table, one row per unit. It carries the leasing funnel counts, the days-on-market dates and the unit's price history, including `last_price_change_date`, `price_reduction_count` and `starting_rent`. It takes the same filters and paging as the other reports.
- `/reporting/units` no longer requires `start` and `end`. It reports current inventory rather than activity over a period, so it has no reporting window; both parameters are still accepted and ignored.
- `/reporting/calls` rows carry `transfer_destination` and `outcome` (`Escalated`, `Success` or `Failure`, or null on a call Voice AI did not handle).
- `/reporting/prospects` rows carry `fraud_flagged` (`Flagged`, `Resolved`, or null when no flag was found for the prospect's phone number) and `account_id`, the account the prospect belongs to.
- `/reporting/leasing-performance/units/{unitId}` returns `last_price_change_date`, when the unit's rent was last changed, and `leads_since_last_price_change`. The date is null if the rent has never changed; the count runs from the listing start in that case, so it is still a number. A change is a change in either direction, and is taken across the unit's whole history rather than the current listing. Neither field is affected by `start`/`end`.
- `/reporting/agent-performance` rows carry `assigned_prospects`, `active_prospects`, `avg_response_minutes`, `lifetime_days_on_market`, `avg_showing_rating` and `showings_rated`.
- A `503` from a `/reporting/*` endpoint carries a `code`: `report_timeout` when the report itself exceeded its time limit, which retrying unchanged will not fix, and `service_unavailable` when a dependency was briefly unavailable and the request is worth retrying. Branch on the value.
- `/reporting/units` is ordered by `unit_id` ascending and `/reporting/applications` by `started_at` descending. Row counts on some reports shift slightly as they come into line with the Intelligence dashboard.

## 1.4.3 — 2026-09-18

- `POST /units` rejects an `account_id` that does not match the unit's current account when an `id` is provided, with a `400` naming the field. A unit cannot be moved between accounts. Sending the unit's own `account_id` on an update is unchanged, and creation still requires the field.

## 1.4.2 — 2026-09-16

- `POST /lockbox_events` documents and enforces `unit_id` as a positive integer, matching the `LockboxEvent` and `LockboxInstallation` schemas. A fractional or non-positive id used to reach the database and come back as a `500`; it is now a `400`. Whole-number ids are unaffected.

## 1.4.1 — 2026-09-03

- Developer portal can mint **read-only** API keys (`read_only` JWT claim). GET/HEAD succeed; POST/PUT/PATCH/DELETE return `403` `{ "error": "This API key is read-only", "code": "read_only_token" }`. Existing keys without the claim keep full access.

## 1.4.0 — 2026-09-02

- `GET /accounts` lists the accounts a token can act on (`account_id`, `name`, `account_role`), so an account can be found by name instead of by ID. Scoped to the token user's active memberships. Unpaginated.

## 1.3.2 — 2026-09-01

- Bearer-authenticated error JSON includes a stable `code` (`validation_error`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `unprocessable_entity`, `rate_limited`, `internal_error`, `service_unavailable`). Existing `error` / `details` / `retryAfter` fields are unchanged.

## 1.3.1 — 2026-09-01

- Bearer-authenticated POST endpoints honor an optional `Idempotency-Key` header (24h replay of 2xx; `409` in-flight; `422` on body mismatch). Unauthenticated POSTs do not.

## 1.3.0 — 2026-08-31

- `GET /units` and `GET /prospects` accept `updated_after` / `updated_before` (ISO-8601) for incremental sync.
- Legacy list endpoints (bare JSON array) now send `X-Has-More` and, when another page exists, `X-Next-Page-Number`. Response bodies are unchanged.
- Bearer-authenticated `/api/public/v1` responses echo `X-Request-Id` (printable ASCII, 1–128 chars); when none is sent we return our internal request id used for tracing. It is the id in RentEngine logs. Use `Idempotency-Key` to prevent duplicate writes.
- Published compatibility policy: [compatibility.md](https://app.rentengine.io/openApi/api/public/v1/compatibility.md).
- Machine-readable overview: [llms.txt](https://app.rentengine.io/llms.txt).

## 1.2.0

- `Unit` (and unit webhook payloads) document `bedrooms`, `bathrooms`, `sqft`, `num_parking_spots`, `target_rental_rate`, and `commission_amount` as numbers. Live responses already returned numbers; this aligns the contract. `POST /units`, `POST /floorplans`, `POST /multifamily_properties`, and `POST /prospects` still accept numeric strings (e.g. `"1925"`) and coerce them to numbers.
- Prospect webhook `unit_of_interest` and `LockboxInstallation.unit_id` are numbers, matching live GET responses and the `Prospect` / `LockboxEvent` schemas.
- `MarketingListing` documents `depositAmountMax` (string or null): the upper bound of a deposit range, or null when the deposit is a single value. `depositAmount` remains the low end.

## 1.1.1

- Prior public API surface. See the OpenAPI spec.
