# Mason External API changelog

Contract changes to the Mason External API, newest first. Each entry names the
OpenAPI specification revisions (`info.version`) it introduced, where one was
published: v3 at `/api/external/v3/openapi.json`, v2 at
`/api/external/v2/openapi.json`. v1 has no published specification. See
"Versioning and compatibility" in the specification for what counts as a
breaking change and how deprecations are announced.

This changelog is served publicly at `GET /api/external/v3/changelog`.

## 2026-10-27 — RentVine deletion reason

Specification revisions: v3 `3-c86cf0ea0ad87db3`, v2 `2-5a97afaf14469c94`.

### Added

- `status_changed` activity details can now carry `reason:
"deleted_in_rentvine"` on `rentvine_sync` events, recorded when Mason
  cancels a work order because it was deleted in RentVine. The timestamp is
  when Mason applied the cancellation, not when the order was deleted.
  `deleted_in_appfolio` on `appfolio_sync` events is unchanged.

## 2026-10-22 — RentVine invoice property identity

Specification revisions: v3 `3-c511f90e95dc9fa5`, v2 `2-a773cfa1451b6e5d`.

### Fixed

- Newly matched and imported RentVine invoices store the Mason property UUID
  in `matchedProperty.propertyId`, allowing the inactive-property approval
  check to resolve the property. `matchedProperty.entityId` remains the numeric
  RentVine property ID as a string and continues to identify the property for
  PMS bill creation. Existing records may still contain the legacy sync-row
  UUID until they are backfilled; clients must not assume that legacy UUID
  identifies a Mason property.

## 2026-10-20 — Not-an-estimate status

Specification revisions: v3 `3-2f684e5bea9648e4`, v2 `2-a21cd1f726643259`.

### Added

- Estimate `status` can now be `not_an_estimate`: Mason judged that the source
  document is not a price quote awaiting approval (for example a move-out
  deposit report), so no decision is pending. Like `superseded`, both
  approval flags stay false. Unlike `superseded`, a later approval or rejection
  still applies and moves the estimate to `approved_for_work` or `rejected`.

## 2026-10-19 — Rent Manager work-order identity

Specification revisions: v3 `3-b2fa6f967add255e`.

### Added

- Work orders linked to Rent Manager now return `pms.system: "rentmanager"`,
  the integration `profileId`, the numeric `locationId`, and the native issue
  ID in `workOrderId`. `workOrderNumber` uses `location:issue` because native
  issue IDs can repeat between locations. Only records owned by your account
  are returned. Other providers omit `locationId`; v1 and v2 are unchanged.

## 2026-10-15 — Inspection appointments

Specification revisions: v3 `3-7dd6305556746f43`.

### Changed

- `POST /scheduling-blocks` and `PATCH /scheduling-blocks/{id}` with `start` and
  `end` now book and move appointments on inspection visits, the same way they
  book and move other in-house visits. They still return `400` `invalid_body`
  for an inspection visit when your account schedules inspections through the
  Mason inspection planner.
- The descriptions of `POST /scheduling-blocks` and `PATCH
/scheduling-blocks/{id}` now say that the request sends the tenant nothing,
  and that Mason's tenant follow-ups may then ask the tenant to confirm the time
  and remind them on the day. This was already the behavior for in-house
  maintenance visits; the earlier descriptions said Mason never messaged the
  tenant.

## 2026-10-13 — Superseded estimates

Specification revisions: v3 `3-e7dea5b44010638a`, v2 `2-5121840fadb7992e`.

### Added

- Estimate `status` returns a new value, `superseded`, for an estimate the
  vendor replaced with a newer estimate on the same work order. A superseded
  estimate no longer awaits a decision, and any earlier approval of it no
  longer authorizes work: `pmApproved` is `false`, while `pmApprovedAt`,
  `pmApprovedBy`, and the approval fields keep the record of that earlier
  decision. The replacement is a separate estimate with its own status.

### Changed

- `PATCH /estimates/{id}` on a superseded estimate returns `409` `conflict` and
  nothing runs. Decide the replacement estimate instead.

## 2026-10-11 — Preventative work-order type

Specification revisions: v3 `3-dcbc54383b4fe87a`, v2 `2-a7904244e9080e08`.

### Added

- `workOrderType` accepts and returns a new value, `preventative`, for one-off
  work that prevents a future maintenance problem and was found proactively
  rather than reported by the tenant, such as inspection follow-ups. Work
  previously returned as `reactive` may now be returned as `preventative`. The
  v2 field description lists the new value.

## 2026-10-01 — Reopening cancelled work orders in the PMS

Specification revision: v3 `3-152482be9d25c31c`.

### Changed

- `PATCH /work-orders/{id}` with a `status` that moves a duplicate, cancelled,
  or not-handled-by-Mason work order out of that status now reopens the work
  order in the property management system first. If the property management
  system cannot reopen it, the request returns `502` `pms_write_failed` and the
  status is unchanged; previously the status changed in Mason and the next
  property management system sync could revert it. The request and response
  shapes are unchanged; the `502` description now lists this case.

## 2026-09-30 — Estimate rejection vendor notice

Specification revision: v3 `3-851d5425e41f8b48`.

### Changed

- `PATCH /estimates/{id}` with `status: "rejected"` no longer sends the vendor
  an immediate rejection email. Mason now queues its vendor follow-up agent,
  which tells the quoting vendor by email on their estimate or dispatch thread
  or by text, usually within a few minutes, and shares your `reason` only when
  it is appropriate for the vendor to read. No notice is sent when Mason is
  stopped on the work order, the account is not live, or Mason has no contact
  for the vendor. The request, response, and status codes are unchanged; the
  operation and `reason` descriptions now state this behavior.

v1 and v2 are unchanged.

## 2026-09-24 — Vendor creation follow-ups

Specification revision: v3 `3-cc5239c7c31cde0e`.

### Fixed

- A `POST /contacts` match against a synced RentVine or AppFolio vendor now
  also checks the other supplied channel (phone number or email) against the
  matched vendor, returning `409` `conflict` naming that vendor when it
  disagrees, instead of matching on one agreeing channel alone.
- An already-linked vendor match is now reactivated and has a missing phone
  number or email filled in, like any other match; previously it was returned
  unchanged.
- If the created vendor cannot be read back, the request now returns `500`
  `internal_error` naming the saved contact id and pointing to
  `GET /contacts/{id}`, instead of a `201`/`200` success carrying a bare
  `{ id }` body that would then be replayed under the Idempotency-Key.
- `CreateContactRequest` is now published as a single strict object requiring
  `contactType: "vendor"` (with the same phone-or-email requirement), rather
  than a one-branch `discriminatedUnion`. The accepted contract is unchanged.

v1 and v2 are unchanged.

## 2026-09-23 — Create vendor contacts

Specification revision: v3 `3-b5597dbf1b916015`.

### Added

- `POST /contacts` creates a vendor contact with a read & write key and a
  unique `Idempotency-Key`. Body: `contactType: "vendor"` and `name`
  (required), at least one of `phoneNumber` or `email`, and optional
  `firstName` and `lastName` for the vendor's contact person (stored by
  AppFolio only). On RentVine or AppFolio, Mason creates the vendor there
  first, so your property management system gets a new vendor record, then
  saves it in Mason; accounts with no connected system get a Mason-only
  vendor. A vendor with the same name and a matching phone number or email,
  in Mason or in Mason's synced copy of your RentVine or AppFolio vendors, is
  reused instead of duplicated and returns `200` with `created: false`; a new
  vendor returns `201` with `created: true`. Both carry the contact in `data`
  and an `ETag` for `PATCH /contacts/{id}`. A partial match, an account on
  PropertyWare, Buildium, or Property Meld, or a test account connected to
  RentVine or AppFolio returns `409` `conflict`; a failed property management
  system write returns `502` `pms_write_failed`. Other contact types return
  `400`.
- `CreateContactRequest` component schema.

### Changed

- The "Records owned by your property management system" overview paragraph,
  the "API keys" paragraph, the `info.description` summary, and the `contacts`
  tag description now name vendor creation with `POST /contacts`.
- `/contacts` answers `OPTIONS` and `405` responses with `Allow: GET, POST`.

v1 and v2 are unchanged.

## 2026-09-23 — API-key documentation, sync recipe, consistent v3 typing, owner rejection

Specification revision: v3 `3-baf323cc332065bd`.

### Added

- An "API keys" paragraph in the specification overview: where to create a key
  (Settings → API access), that any user on the account can create one, the
  read-only and read & write scopes, one-time secret display, rotation and
  revocation, and how a key's account (Production or Test) is fixed by the
  property-manager record it was created under. The `bearerAuth` security
  scheme description points to it and names the two scopes.
- A "Syncing data" paragraph documenting the incremental-sync recipe
  (`updatedAfter`/`createdAfter` + `pagination.nextCursor` + an overlap window)
  and, per resource, which fields keep changing after `createdAt` so
  integrators can size that overlap.
- `PATCH /estimates/{id}` accepts an optional `rejectedByRole: "pm" | "owner"`
  with `status: "rejected"`, defaulting to `"pm"`. Send `"owner"` to record a
  rejection the property owner already gave outside Mason, for example in
  your owner portal. It is recorded as the actor role on the work order's
  `estimate_rejected` activity event; the estimate record itself has no
  rejection-role column. On an estimate that is already rejected the request
  returns `200` unchanged regardless of `rejectedByRole`.
- `GET /work-order-activity-logs` includes `details.actorRole` and
  `details.actorName` on `estimate_approved` and `estimate_rejected` events
  when an actor was recorded.

### Changed

- List endpoints: the incremental-pagination sentence now states the keyset
  traversal guarantee precisely (ordering, stability, current-value filtering,
  and how a change made after a page was read is picked up by the next
  incremental run rather than the current one).
- The "Versioning and compatibility" paragraph states that narrowing a field's
  declared type to one of the types it already permitted is a tightening, not
  a breaking change.
- v3 only: nested fields that were declared as a union of JSON types now have
  one declared type, and v3 returns every value in that type. v1 and v2 are
  unchanged and still return the stored JSON.
  - `Invoice.items[]` and `Estimate.lineItems[]` `quantity`, `unitPrice`,
    `totalPrice`; `Property.ownerInfo[]` and `Unit.ownerInfo[]`
    `ownershipPercentage`; `Property.propertyDetails` `lat`, `long`,
    `yearBuilt`, `numberOfFloors`: `number | null`. Stored text is read as a
    number after removing a currency sign, grouped-thousands commas, and a
    trailing `%`, and `(45.00)` is `-45`, so `"$1,234.50"` is `1234.5` and
    `"100.00%"` is `100`. Text that is not a number, such as `""`, `"N/A"`, or
    `"2 hrs"`, is `null`. Invoice line amounts stay in the document's own
    units; they are not converted to cents.
  - `Property.propertyDetails` `multiUnit` and `readyToLease`:
    `boolean | null`. Stored `Yes`/`No`, `true`/`false`, and `1`/`0` are
    converted; other values are `null`.
  - `entityId` and `vendorId` on `Invoice.matchedVendor`, `entityId` and
    `propertyId` on `Invoice.matchedProperty`, and `entityId` and
    `workOrderId` on `Invoice.matchedWorkOrder`: `string | null`. A numeric
    identifier is returned as a string.
  - `address` in `Invoice.vendorInfo`, `Invoice.billToInfo`, and
    `Invoice.matchedVendor.vendorInfo`: `string | null`. A structured address
    is returned as one line.
  - `Invoice.matchedWorkOrder.workOrderGrammaticalSummary` is declared
    `string | null`; it was declared `string` but is null on most records.
  - Array members that are not objects are omitted from `Invoice.items[]`,
    `Property.ownerInfo[]`, and `Unit.ownerInfo[]` in v3, and are `null` in
    `Estimate.lineItems[]`; a non-object where an object is declared
    (for example a plain-string `Invoice.vendorInfo`) is `null`.
- Every non-null value v3 now returns has a type the previous union allowed.
  The `null` branch documents nulls v3 already returned for stored JSON nulls
  (for example `matchedVendor.vendorId`, `propertyDetails.yearBuilt`, and
  invoice line `quantity`).
- The specification overview no longer says stored JSON "retains its existing
  shape"; it now says keys are retained and, in v3, nested values are returned
  in their single declared type.

### Breaking

- v3 only: `Invoice.items[]` `quantity`, `unitPrice`, and `totalPrice` text
  that is not a number (about 11,000 of 335,000 stored line values, mostly
  empty strings and the text `"null"`, plus values such as `"N/A"` and
  `"2 hrs"`) is now `null` instead of the stored string; the same rule applies
  to `Estimate.lineItems[]` `quantity`, `unitPrice`, and `totalPrice`. Under
  the "Versioning and compatibility" policy above, changing a field's returned
  type is breaking; this one ships inside v3 rather than a new path version
  because v3's write API only launched 2026-09-22, and every affected value is
  stored text no client could have parsed as a numeric amount. Read v2 for the
  stored text.

v1 and v2 are unchanged.

## 2026-09-23 — Mason-maintained fields on contacts, units, and properties

Specification revision: v3 `3-ae13945dd66bc899`.

### Added

- `PATCH /contacts/{id}` changes a contact's Mason-maintained
  `preferredContactMethod`, `language`, `stopAllComms`, and `notes`.
- `PATCH /units/{id}` changes a unit's Mason-maintained `access` (`gateCode`,
  `lockboxCode`, `alarmCode`, `notes`), merged key by key: an omitted key is
  left unchanged and `null` clears it.
- `PATCH /properties/{id}` changes a property's Mason-maintained `access`
  (same merge semantics) and `pauseOwnerOutreach`.
- `GET` and `PATCH` on `/contacts/{id}`, `/units/{id}`, and `/properties/{id}`
  return an `ETag` derived from the record's `updatedAt`; send it as
  `If-Match` on `PATCH` to get `412` instead of changing a record that
  changed since you read it.
- New read fields: `Contact.stopAllComms` and `Contact.notes`; `Unit.access`;
  `Property.access` and `Property.pauseOwnerOutreach`.
- All three writes require a read & write API key. Access details are not yet
  included in the dispatch messages Mason sends vendors and technicians;
  that is a separate change.

### Changed

- `Contact.preferredContactMethod` and `Contact.language` field descriptions
  now name their values (such as `phone`, `text`, `email`, and language codes
  like `en` or `es`) and note that Mason maintains them.
- The `contacts`, `units`, and `properties` resource descriptions, and the
  specification overview's "Records owned by your property management
  system" paragraph, now distinguish fields your property management system
  owns (read-only here) from fields Mason maintains (changeable with PATCH).

### Fixed

- The `Contact.preferredContactMethod` example said `sms`; the field's only
  valid values are `phone`, `text`, and `email`, and the example now reads
  `text`.

## 2026-09-23 — Owner approval on estimate decisions

Specification revision: v3 `3-49fdd202cd9e5800`.

### Added

- `PATCH /estimates/{id}` accepts an optional `approvedByRole: "pm" | "owner"`
  with `status: "approved"`, defaulting to `"pm"`. Send `"owner"` to record an
  approval the property owner already gave outside Mason, for example in your
  owner portal; Mason then sends the owner no "work is proceeding" email, but
  otherwise processes the approval the same way. When you send
  `approvedByRole` and the estimate is already approved with a different
  recorded `approvedByRole`, the request returns `409` with code `conflict`
  and nothing runs. Omitting `approvedByRole` keeps the previous behavior: an
  already-approved estimate returns `200` unchanged.

## 2026-09-22 — Error codes, request IDs, numeric invoice percentages, examples, compatibility policy

Specification revisions: v3 `3-3fc2715d83e4508b`, v2 `2-68a0e538d822c82b`.

### Added

- Every error response now includes a stable machine-readable `code` and the
  `requestId`, alongside the existing `error` message, on v1, v2, and v3. The
  Error schema lists every code and its meaning. This is additive: existing
  fields are unchanged.
- Every business-data response, successful or not, carries an
  `X-Mason-Request-Id` header. Send a well-formed `X-Request-Id` (8-128
  letters, digits, periods, underscores, colons, or hyphens) to use your own
  ID; otherwise Mason generates a UUID. Quote it when contacting Mason support.
- `403` from `POST /work-orders` before the account is live in Mason now uses
  code `account_not_live`. Work-order creation is part of every Mason plan at
  no separate charge.
- Worked request and response examples for every v3 operation, including
  error cases, and field-level examples on every v3 resource schema.
- A "Versioning and compatibility" policy and a note on records owned by your
  property management system in the specification overview.
- This changelog, at `GET /api/external/v3/changelog`.
- An unsupported HTTP method on documented v3 paths, such as
  `DELETE /work-orders/{id}`, returns `405` with code `method_not_allowed`, an
  `Allow` header listing the methods the path supports, and the
  `X-Mason-Request-Id` header, instead of an empty `405`. A path below
  `/{resource}/{id}`, such as `POST /scheduling-blocks/{id}/cancel`, matches no
  endpoint and returns a JSON `404` with code `unknown_resource` for every
  method.

### Fixed

- `SchedulingBlock.start` and `SchedulingBlock.end` are ISO 8601 UTC strings
  (such as `2026-09-01T12:00:00.000Z`), as documented, on v2 and v3, in list
  and detail reads and in scheduling-block write responses. They were returned
  as database text such as `2026-09-01 12:00:00+00`. v1 is unchanged.

### Breaking

- v3 list endpoints reject a filter value outside the parameter's documented
  enum with `400` and code `invalid_parameter`; the message names the parameter
  and its allowed values. Today this applies to `priority` on
  `GET /work-orders` (`low`, `normal`, `urgent`). Such values previously
  returned `200` with no records, so no request that could match records is
  affected, but a request accepted before is now rejected, which the
  compatibility policy classifies as breaking.
- v3 only: `Invoice.selectedMarkupPercentage` and
  `Invoice.selectedDiscountPercentage` are JSON numbers (percent; `10` means
  ten percent) instead of decimal strings. A stored decimal, with or without a
  trailing `%` (such as `10.00` or `10.00%`), returns that number; any other
  stored text returns `null`. v2 still returns the stored string unchanged.
  This change was made before the compatibility policy was published; later
  type changes follow that policy and ship only in a new path version.

## 2026-09-22 — Estimate and invoice decisions, appointment writes (#9032)

Specification revision: v3 `3-6328535bec27dcca`.

### Added

- `PATCH /estimates/{id}` approves or rejects an estimate: send
  `status: "approved"`, with optional `approvedAmount` (integer cents),
  `approvedScope`, and `skipReschedule`, or `status: "rejected"` with an
  optional `reason`. An approved estimate reads back as
  `status: "approved_for_work"`.
- `PATCH /invoices/{id}` approves an invoice to pay or rejects it: send
  `decision: "approved"`, with optional `confirmMissingInvoiceNumber` and
  `confirmInactiveProperty`, or `decision: "rejected"` with an optional
  `reason`. The invoice's `status` field is its processing state and does not
  change; `pmApproved` and `pmRejected` record the decision.
- Both decisions are naturally idempotent and take no `Idempotency-Key`:
  sending the decision the record already has returns `200` with the current
  record and runs nothing; sending the opposite decision reverses the earlier
  one, as in Mason. A field that belongs to the other decision returns `400`.
- `GET` and `PATCH` on `/estimates/{id}` and `/invoices/{id}` return an `ETag`
  derived from the record's `updatedAt`; send it as `If-Match` on `PATCH` to
  get `412` instead of deciding a record that changed since you read it.
- `POST /scheduling-blocks` books an in-house visit appointment and requires
  an `Idempotency-Key`.
- `PATCH /scheduling-blocks/{id}` moves or reassigns an appointment (`start`
  and `end` and/or `technicianContactId`, with optional `notes`) or cancels it
  (`blockType: "canceled"`, one L, with an optional `reason`); fields from the
  two forms cannot be combined. It takes no `Idempotency-Key`. Cancelling a
  block that is already canceled returns `200` with the block and runs
  nothing. The response is `{ data, pmsSyncError }`; `pmsSyncError` is always
  `null` for a cancellation.
- All of these writes require a read & write key.

## 2026-09-22 — Work-order assignment fixes (#9033)

Specification revision: v3 `3-a3d380e0ac6c4786` (as merged to main).

### Changed

- `PATCH /work-orders/{id}` `assignment`: a technician who is already the
  active assignee is refused with `409` (previously only vendors were);
  inactive contacts are refused with `400`; gate and lockbox codes are sent
  with the dispatch as the app does; vendors whose stored preference is text
  are dispatched by text; a technician assignment the property management
  system rejects returns `200` with `pmsAssignmentError` instead of a silent
  `200`.
- The `assignment` description states its side effects, including that a
  technician assignment can text the tenant a scheduling link when tenant
  scheduling is enabled, and lists where an API assignment differs from a
  manual dispatch in the app.

---

This changelog starts with the September 2026 release above. Earlier changes
(the v2 contract in #8332, the published specification in #8787, v3 in #8797,
self-serve keys in #8977, rate limits in #8995, work-order create and update in
#8998, self-serve revocation in #9025, and work-order assignment with
ETag/If-Match in #9029) are not itemized; `info.version` identifies the exact
specification revision in effect at any time.
