# Changelog

The API is in beta, and it can change without advance notice. This page records every change. Watch
it, and you see what moved, and when it moved.

The newest entry is first. Each date is the day that the change reached production.

## 2026-10-27

- **Added:** `buydown_reason` on a commission, the broker's note on why they bought the rate down.
  You can read it, and you can set it with `POST` or `PATCH /v1/deals/{id}/commission`. It is optional.

## 2026-10-21

- **Added:** a contact relationship's `type` can be a professional the client keeps: `realtor`,
  `lawyer`, `appraiser`, `accountant`, `financial_planner`, `insurance` or `builder`. The `label` is
  worded from the contact you read, so the professional reads `Lawyer` and the client reads `Client`.
- **Added:** `GET /v1/opportunities` takes the filters from the book in BrokerPlus: `dismissed`,
  `archived`, `maturity_from` and `maturity_to` (a maturity window, `YYYY-MM-DD`), `opportunity_type`,
  `product_type`, `start_year`, and `assigned_broker_id`. It also takes `sort`, which orders the list
  by maturity date or by the client's last message. A list filter takes values separated by commas,
  and an unknown value gets `400 VALIDATION_ERROR`.
- **Added:** `mode` on a `field`, `checkpoint`, or `custom_field` requirement of a pipeline stage.
  `cumulative` is the default and applies the requirement to the stage and to every later stage, as
  before. `this_stage` applies it to the stage only. `optional` asks the broker for it when they move
  a deal into the stage, but does not stop the move. If you read requirements and send them back,
  keep `mode`, or an optional requirement becomes required.
- **Changed:** a `checkpoint` requirement with `required: false` now reads as `mode: "optional"`. The
  broker sees the step when they move a deal into the stage, and the move still goes ahead. A `mode`
  that you send on a `checkpoint` wins over `required`.
- **Added:** a stage requirement of kind `custom_field`. Its `key` is one of your deal custom
  fields, and the deal must have an answer before it moves into the stage. A checkbox counts only
  when it is ticked. `GET /v1/deals/{id}/stage-requirements` and a `409 STAGE_REQUIREMENTS_UNMET`
  name a missing custom field with `kind: "custom_field"`.
- **Added:** each `province` that takes a numeric code also takes the two-letter code, for example
  `ON` in place of `50`. This applies to the subject property, an employment, and an applicant
  address. On read, these provinces also have `abbreviation`, the two-letter code.
- **Changed:** an other property's `province` takes only a province: the two-letter code, or the
  numeric code, which is saved as the two-letter code. Any other value gets `400 VALIDATION_ERROR`,
  and the error lists the accepted values. It used to save any text.
- **Added:** `PATCH /v1/deals/{id}` takes `type`, with the same values as `POST /v1/deals`. The change
  is the same as in the dashboard: BrokerPlus calculates the total loan amount and the mortgage
  insurance premium again, and a change that crosses a renewal type replaces the renewal disclosures.
  The deal stays on its pipeline and in its stage.
- **Added:** `GET /v1/deals` takes `stage_id` and `board_id`. `stage_id` returns only the deals in that
  stage, and `board_id` returns only the deals on that pipeline. Use them with `updated_since` to sync
  one stage.
- **Changed:** `POST /v1/opportunities` no longer requires `term_months`. Without a term, BrokerPlus
  does not estimate the balance. The opportunity uses the `balance` that you send, or `mortgage_amount`
  when you do not send `balance`.
- **Added:** an automation's email step can name a role on the deal in `toRecipients`,
  `ccRecipients` and `bccRecipients`, for example `deal-role:Closing Officer`. The email goes to the
  teammate who holds that role on the deal. Only a trigger that runs on a deal accepts it.

## 2026-10-20

- **Changed:** an automation's `type_label`, and each trigger's name in `GET /v1/automations/catalog`,
  is now in title case, the same as on screen. `Deal moved into a stage` reads `Deal Moved into a Stage`.
  The `type` values did not change, so match on `type` rather than on the name.

## 2026-10-19

- **Added:** a captured answer's `source` can be `meeting`. It is an answer the client gave in a
  meeting that the notetaker recorded, and its `communication_id` is that meeting.
- **Added:** a lead's `source` and a contact's `created_via` can be `assistant`: a person the broker's
  assistant added to their book.
- **Changed:** `POST /v1/contacts` with a name and a phone number fills in the contact that holds only
  that number, if there is one, and returns it. It used to add a second contact beside it.
- **Changed:** a communication folder from an Outlook mailbox can now have a `parent_id`. Outlook
  folders used to arrive with `parent_id` set to null. They now name the folder they sit in, the same
  as in Outlook, and that folder can be a system folder such as the inbox.

## 2026-10-17

- **Changed:** an employment's `type` no longer has the temporary codes. The codes are now 1 (permanent
  full time), 2 (permanent part time), 5 (seasonal) and 7 (not applicable). A job saved as temporary full
  time (3) now reads as 1, temporary part time (4) as 2, and temporary seasonal (6) as 5. A request that
  sends 3, 4 or 6 is still accepted, and saves 1, 2 or 5 in the same way.
- **Added:** a lead's `source` and a contact's `created_via` can be `meeting`: a person the broker
  met in a meeting and then added to their book.

## 2026-10-13

- **Added:** an outreach template returns `body_html`, the formatted version of an email template.
  `PATCH /v1/outreach-templates/{id}` takes `body_html` to change it, or null to go back to plain
  text. A new `body` without a new `body_html` still removes the formatted version.
- **Added:** a credit report's `status` can be `no_score`. It is a credit check that the system the deal
  came from ran, which returned no score. You can still pull credit for that borrower.
- **Changed:** on a second or later charge against a home the borrower already owns, `ltv` on
  `GET /v1/deals/{id}/application` is the combined loan to value (CLTV). It counts the balance of each
  mortgage and home line of credit ahead of the loan, the way a lender reads it. On a first charge it
  is the loan alone, as before. When the deal cannot say which charge is ahead, `ltv` stays the loan
  alone.
- **Added:** `GET` and `PATCH /v1/deals/{id}/property` return `lending_value_basis` and
  `estimated_value`. When a file names the value it lends against (`price`, `estimate`, or
  `appraisal`), `ltv` on `GET /v1/deals/{id}/application` is worked out on that value while the deal
  holds it, on a refinance, switch or renewal. A purchase, and any file that names no value, is still
  worked out on the lower of the purchase price and the appraised value.
- **Changed:** when `loan_amount` on `GET /v1/deals/{id}/application` already holds the default
  insurance premium, `total_loan_amount` is `loan_amount` and the premium is not added a second time.
  A loan holds it when it is within one dollar of the purchase price less the down payment plus the
  premium, or of Finmo's own total for the loan. `insurance_premium_rate` is then a percent of the loan
  before the premium, which is `total_loan_amount` minus `insurance_premium`. With no property value, a
  premium on file is added only on a deal that started in Finmo. On any other deal `total_loan_amount` is
  then `loan_amount`.
- **Fixed:** ChatGPT finishes connecting to the [MCP server](/mcp). A client whose metadata document
  names `private_key_jwt` and also lists `none` in `token_endpoint_auth_methods_supported` can get
  its token with PKCE alone. If it sends a `client_assertion`, the assertion is checked.
- **Fixed:** a connected app keeps its connection while its metadata document can't load. A public
  client's refresh no longer reads the document. A `private_key_jwt` client uses the last keys
  that loaded, for up to 24 hours.

## 2026-10-12

- **Breaking:** a key with the `no_sending` limit now gets `403 KEY_LIMITED` on the requests that
  show a client something at once: `POST /v1/hub-resources`, `POST /v1/tracker-stages`,
  `PATCH /v1/tracker-stages/{id}`, `POST /v1/tracker-stages/reorder`, `POST /v1/tracker-stages/seed`,
  and `PATCH /v1/tracker-stages/note`. Reading and removing stays open. See
  [Reaching clients](/reaching-clients).
- **Changed:** the [MCP server](/mcp) covers the whole API. Every operation is now a tool, deletes,
  setup, webhooks, batch writes, signed links, and compliance records included. The tool's MCP
  annotations tell your assistant what to confirm with you: a delete is marked destructive, and a
  tool that reaches a client or anybody else outside the brokerage is marked destructive and
  open-world.
- **Changed:** the MCP server hides every tool that the key's limits refuse. A key with the
  `no_sending` limit no longer sees the tools that reach a client, and a read-only key still sees only
  the tools that read. The API refuses these calls the same way as before.
- **Changed:** the MCP tool list is under 1 MB, so Claude and ChatGPT connectors load every tool. A
  listed `outputSchema` names only `data` and `pagination`, and descriptions stop after their first
  sentences. `brokerplus_describe_operation` returns the full contract of any operation.
- **Changed:** ChatGPT connects to the [MCP server](/mcp). The authorization server now accepts
  `private_key_jwt` client authentication from a client that declares it in its Client ID Metadata
  Document, and it lists the method in `token_endpoint_auth_methods_supported`.
- **Added:** reply to a client. `POST /v1/communications/{id}/reply` answers an email or text in its
  own conversation, from the mailbox or number the conversation is on, with `mode` `reply`,
  `reply_all`, or `forward`, and files in `attachments`. It reaches the client, so a key with the
  `no_sending` limit is refused it. `POST /v1/communications/{id}/drafts` writes the same reply into
  the broker's drafts and sends nothing.
- **Added:** `PATCH /v1/drafts/{id}` edits a draft, `POST /v1/drafts/{id}/discard` discards one, and
  `POST /v1/drafts/{id}/schedule` and `POST /v1/drafts/{id}/unschedule` set and stop a send time.
  Scheduling reaches the client, so a key with the `no_sending` limit is refused it.
- **Added:** `send_at` on `POST /v1/contacts/{id}/messages` and `POST /v1/communications/{id}/reply`
  sends the message later.
- **Added:** the draft object returns `to`, `cc`, `bcc`, `reply_to_communication_id`, `forward`, and
  `attachments`.
- **Added:** a forward on `POST /v1/communications/{id}/reply` and `POST /v1/communications/{id}/drafts`
  carries the files of the email it forwards. Send `include_attachments: false` to leave them out.
- **Changed:** the draft object's `contact_id` is null for a reply or forward to an address that is
  not a contact. It was always a contact before.
- **Changed:** `POST /v1/drafts/{id}/send` also sends a draft with no contact, such as a reply to a
  lender.

## 2026-10-11

- **Added:** a campaign can go to several audiences. `segment_ids` on `POST /v1/campaigns` and
  `PATCH /v1/campaigns/{id}` takes up to 20 more audiences next to `segment_id`, and a person in more
  than one of them gets one message. The campaign object returns every audience in `segment_ids`;
  `segment_id` is now the first of them.
- **Changed:** a recipient variable such as `{{borrower.firstName}}` no longer needs a fallback. A
  contact with no value gets a blank.
- **Changed:** `POST /v1/campaigns/{id}/schedule` returns `422` with `SENDER_DETAIL_MISSING` when the
  campaign names a sender detail, such as `{{broker.phone}}`, that the broker's profile leaves empty.

## 2026-10-10

- The notetaker can be sent from the API: `POST /v1/meetings` sends it into a call,
  `POST /v1/meetings/{id}/stop` takes it out, and `POST /v1/meetings/{id}/sync` pulls the transcript
  again. Sending it reaches clients, so a key with the `no_sending` limit is refused it.
- Date triggers can be set up and switched on and off: `POST /v1/pulse-triggers` (lands off),
  `PATCH /v1/pulse-triggers/{id}` (moves `days_offset`), `POST /v1/pulse-triggers/{id}/turn-on`, and
  `POST /v1/pulse-triggers/{id}/turn-off`. Turning one on reaches clients, so a key with the
  `no_sending` limit is refused it.
- The reference now describes the `urgent`, `normal`, and `low` fields of a compliance program's
  `alert_routing`, and `GET /v1/intakes/{id}/notes/{noteId}`. Nothing about them changed.
- **Breaking: requests that reach a client act at once, and approvals are gone.** A request that
  emails, texts, books, or otherwise reaches a client no longer waits for the broker's approval. It
  runs now, as the broker your key acts as, and returns the record it made or changed. A key with
  the `no_sending` limit is still refused every one of these requests, so use that limit for any
  key that must not contact clients. See [Reaching clients](/reaching-clients). What changed:
  - `GET /v1/approvals`, `GET /v1/approvals/{id}`, and `POST /v1/approvals/{id}/cancel` are removed,
    with the approval object and the `approval.approved` and `approval.rejected` webhooks.
  - These returned `202` with an approval, and now return `201` with the new record:
    `POST /v1/contacts/{id}/messages` (the sent draft), `POST /v1/deals/{id}/condition-requests`
    (the sent draft), `POST /v1/deals/{id}/generated-documents/{documentId}/send` (the sent draft),
    `POST /v1/mail-merges` (the mail merge), `POST /v1/appointments/book` (the booking),
    `POST /v1/nps-surveys` (the survey), `POST /v1/pep-forms` (the form),
    `POST /v1/deals/{id}/consent-forms` (what was sent), `POST /v1/deals/{id}/document-requests`
    (the document requests it added), and `POST /v1/deals/{id}/funding-package/send` (the signing
    envelope).
  - These returned `202` with an approval, and now return `200` with the record they changed:
    `POST /v1/drafts/{id}/send` (the draft), `POST /v1/automation-runs/{id}/start` (the run),
    `POST /v1/campaigns/{id}/schedule` (the campaign, with `warnings`),
    `POST /v1/appointments/{id}/reschedule` and `POST /v1/appointments/{id}/cancel` (the booking),
    `POST /v1/intakes/{id}/request-changes` (the intake),
    `POST /v1/deals/{id}/signing-documents/send` (the signing documents),
    `POST /v1/deals/{id}/signing-documents/{documentId}/resend` (the signing document),
    `POST /v1/deals/{id}/tracker/{stageId}/reach` (the tracker),
    `POST /v1/automations/stops/{id}/lift` (`{ id, deleted: true }`), and
    `POST /v1/equity-reports/enroll`, `/resume`, and `/send` (a line for each deal, `changed`,
    `unchanged`, or `refused`).
  - `POST /v1/automations/{id}/turn-on` and `POST /v1/lead-forms/{id}/turn-on-auto-reply` always
    return `200` with the automation or the form. `POST /v1/automations/canvases/{id}/turn-on` turns
    on every automation in the group, and its results no longer carry `approval_id` or
    `approval_requested`.
  - `POST /v1/intakes` with `delivery: "email"` emails the intake at once, and the response no
    longer has `approval_id`.
  - Two new errors: `403 FORBIDDEN` when the person your key acts as cannot send this in BrokerPlus
    either, and `409 NOT_SENT` when the request could not go out as it stands. Nothing was sent in
    either case.
  - Your brokerage's automation policy still applies: where it does not allow messages to clients to
    be asked for this way, each of these requests answers `403 POLICY_BLOCKED` and sends nothing.
  - MCP tools that reach a client are now marked `destructiveHint` and `openWorldHint`, so an
    assistant asks its user before it calls one.

- **A merged deal stays archived.** When BrokerPlus merges a spreadsheet copy of a Finmo file into
  the Finmo deal, the copy is archived and its work moves to the Finmo deal. `POST /v1/deals/{id}/restore`
  on that copy returns `409` with the code `MERGED`, and the copy stays archived.
- **Every Canada Post street type.** `street_type` on an address now takes and returns every Canada
  Post street type, English and French, 182 in all, where it took 26. The
  numeric codes it already used keep their meaning.
- **Notes on an intake.** `GET /v1/intakes/{id}/notes` lists the notes brokers wrote on an
  intake's files and items, `GET /v1/intakes/{id}/notes/{noteId}` reads one, and
  `POST /v1/intakes/{id}/notes` writes one or replies to one. The client never sees them. The MCP
  server has a tool for each. See [Write notes on an intake](/intake#write-notes-on-an-intake).
- **Bank statement transactions.** `GET /v1/intakes/{id}/transactions` lists the transactions on a
  client's bank statements with the broker's changes, `POST /v1/intakes/{id}/transactions/override`
  takes them out of the income calculation or recategorizes them, and
  `DELETE /v1/intakes/{id}/transactions/{transactionId}/override` undoes that. Nothing goes to the
  client, and an account number is only ever its last 4 digits. The MCP server has a tool for each
  but the undo. See [Take out or recategorize a transaction](/intake#take-out-or-recategorize-a-transaction).
- **What the income calculation counts.** `GET /v1/intakes/{id}/stated-income-scope` reads the
  broker's choices on the stated income card (the dates, the statements and accounts left out, the
  income the client declared) and the bank accounts on the statements, and `PATCH` changes them.
  Nothing goes to the client. The MCP server has a tool for each. See
  [Shape the income calculation](/intake#shape-the-income-calculation).
- **Tracker links.** `GET /v1/deals/{id}/tracker/links` lists the links that still show a deal's
  tracker to a realtor or a lawyer, and `POST /v1/deals/{id}/tracker/links/{linkId}/revoke` takes one
  back. BrokerPlus no longer makes new ones: a partner gets a partner hub. The MCP server has a tool
  for each. See [Tracker links](/workspace#tracker-links).
- **Media library.** `/v1/media-assets` lists the pictures and video stills the brokerage keeps for
  outreach emails, uploads a picture, adds a video link, and deletes one. The MCP server has a tool
  for each but delete. See [Media library](/templates#media-library).
- **Outreach templates.** `/v1/outreach-templates` reads and writes the first-touch emails and
  texts a broker sends by hand from the refinance, renewal and lead lanes: the broker's own set and
  their team's, with set-primary, duplicate, share and copy-to-personal, as the outreach templates
  editor has. Nothing sends. The MCP server has a tool for each but delete. See
  [Outreach templates](/templates#outreach-templates).
- **Retention signals.** `GET /v1/pulse-signals` lists the reasons BrokerPlus raised to reach a
  past client now, an equity drop or a date of theirs, and `GET /v1/pulse-triggers` reads the
  broker's date triggers. Both are read-only. The MCP server has a tool for each. See
  [Retention signals](/equity-reports#retention-signals).
- **A locked deal refuses opportunity changes.** `PATCH /v1/opportunities/{id}` on a deal in a stage
  that locks it now answers `403 DEAL_LOCKED` for every figure, not only `segments`, as BrokerPlus
  does. `name` and the rate assumptions (`comparison_rate`, `effective_penalty`, `penalty_type`,
  `cashback_enabled`, `cashback_override`) can still change.
- **Recorded meetings.** `GET /v1/meetings` lists the video meetings BrokerPlus took notes in, with
  its summary, and `GET /v1/meetings/{id}` adds the transcript, who spoke and for how long, the
  follow-ups, and the meeting chat. The recording itself is not on the API. The MCP server has a
  tool for each. See [Read recorded meetings](/log-calls#read-recorded-meetings).
- **An automation run's steps.** `GET /v1/automation-runs/{id}/steps` lists what each step of a run
  did or will do: the draft it wrote, the message it sent, the task it made, or why it was skipped.
  The MCP server has a tool for it. See [Read the runs](/automations#read-the-runs).
- **Creditor life insurance.** `GET /v1/deals/{id}/life-insurance` reads the deal's creditor life
  insurance, and `POST /v1/deals/{id}/life-insurance/outcome` records what the client said when the
  broker offered it. Nothing is sent to the insurer. The MCP server has a tool for each. See
  [Creditor life insurance](/model#creditor-life-insurance).
- **Credit consents lapse after 30 days.** A `credit_pull` consent now always has an `expires_at`,
  30 days after `granted_at`, however it was captured. On `POST /v1/deals/{id}/consents`, an earlier
  `expires_at` is kept and a later one is cut to 30 days. Credit consents that had no expiry took the
  same 30 days from their own grant date, so one given more than 30 days ago now reads as expired.
- **Clients' equity report corrections.** `GET /v1/equity-report-changes` lists the corrections
  clients made to their equity reports, and `POST /v1/equity-report-changes/{id}/accept` and
  `/decline` answer one waiting for the broker, as the report changes queue does. Neither sends
  anything to the client. The MCP server has a tool for each. See
  [Answer a client's correction](/equity-reports#answer-a-clients-correction).
- **Send a funding package for signature.** `POST /v1/deals/{id}/funding-package/send` asks the
  broker to send the package. The response is `202` with a pending `funding_package.send` approval,
  bound to the package as it is. `GET /v1/deals/{id}/signing-envelopes` and
  `GET /v1/deals/{id}/signing-envelopes/{envelopeId}` read the envelopes that went out, each with
  its `signing_envelope_recipient` list. The MCP server has a tool for each. See
  [Send it for signature](/files#send-it-for-signature).
- **Mail merges.** `POST /v1/mail-merges` asks the broker to send one email to each address on a
  list, each person on their own copy. The response is `202` with a pending `mail_merge.send`
  approval that covers the whole list. `GET /v1/mail-merges`, `GET /v1/mail-merges/{id}` and
  `GET /v1/mail-merges/{id}/recipients` show what became of each address. An approval's
  `target.type` can now be `user`. The MCP server has a tool for each. See
  [Mail merges](/mail#mail-merges).
- **Credit notes.** `GET /v1/deals/{id}/credit-notes` lists the broker's notes on the credit of the
  people on a deal, and `PATCH /v1/deals/{id}/credit-notes/{contactId}` writes one. Only the broker's
  words are returned: no credit score or report. The MCP server has a tool for each. See
  [Credit notes](/model#credit-notes).
- **Consent forms.** `GET /v1/deals/{id}/consent-forms` and `GET /v1/deals/{id}/consent-forms/{formId}`
  read the consent forms emailed to the borrowers on a deal: the scope, the words, the status, and
  the consent that a signature recorded. `POST /v1/deals/{id}/consent-forms` asks the broker to email
  one: the response is `202` with a pending `consent_form.send` approval. The MCP server has a tool for
  each. See [Ask a borrower for consent](/client-asks#ask-a-borrower-for-consent).
- **What a client said on a call.** `GET /v1/contacts/{id}/captured-answers` lists the intake
  answers BrokerPlus heard on a call or in an email, with what the client said word for word in
  `heard_as`. Add `pending=true` for the ones the next intake will fill in. The MCP server has a
  tool for it. See [Read what the client said on a call](/intake#read-what-the-client-said-on-a-call).
- **Goals.** `GET`, `POST /v1/goals` and `GET`, `PATCH`, `DELETE /v1/goals/{id}` read and set the
  broker's goals from the Business pages: funded volume, funded deals or commission over a period,
  for the whole book or one lender. Each goal carries `pace`, where the broker stands today. The MCP
  server has a tool for each, except delete. See [Goals](/broker-view#goals).
- **Introductions to a partner.** `GET`, `POST /v1/partner-introductions` and `GET`,
  `DELETE /v1/partner-introductions/{id}` read and record the clients and leads a broker introduced to
  a referral partner. Nothing is sent. The MCP server has a tool for each, except delete. See
  [Introductions to a partner](/workspace#introductions-to-a-partner).
- **Partner firm notes.** `GET`, `POST /v1/partner-company-notes` and `GET`, `PATCH`,
  `DELETE /v1/partner-company-notes/{id}` read and write the brokerage's note on each partner firm,
  as on the Partners page. The MCP server has a tool for each, except delete. See
  [Partner firm notes](/workspace#partner-firm-notes).
- **Imports.** `GET /v1/import-runs` and `GET /v1/import-runs/{id}` follow the imports on the
  broker's teams, and `POST /v1/import-runs/files` adds a deals file to the team's open import.
  Nothing reaches the book until the broker reviews the import in BrokerPlus.
  `GET` and `PATCH /v1/import-status-routes` read and save where an import puts a deal by its status
  word. `GET /v1/import-mappings`, `GET`, `PATCH` and `DELETE /v1/import-mappings/{id}` read and
  change the column mappings saved from past imports; only an admin's key changes one. The MCP
  server has a tool for each, except delete. See [Imports](/reports-and-data#imports).
- **Data exports.** `POST /v1/data-exports` requests an export of one package of the broker's
  records, `GET /v1/data-exports` and `GET /v1/data-exports/{id}` follow it, and
  `POST /v1/data-exports/{id}/cancel` stops it. `GET /v1/data-exports/packages` lists the packages
  you can export.
  The broker downloads the files in BrokerPlus. The MCP server has a tool for each. See
  [Data exports](/reports-and-data#data-exports).
- **Team chat.** `GET /v1/direct-threads` lists the broker's team chat conversations, and
  `POST /v1/direct-threads` opens one with a teammate or starts a group.
  `GET` and `POST /v1/direct-threads/{id}/messages` read and post messages, and
  `POST /v1/direct-threads/{id}/read` marks a conversation read. `GET /v1/direct-threads/roster`
  lists who the broker can message. A key reaches only the conversations its broker is in. The MCP
  server has a tool for each. See [Team chat](/workspace#team-chat).
- **Documents on a condition.** A condition now lists the documents attached to it in `documents`,
  and `POST /v1/conditions/{id}/documents` attaches more from the same deal. Nothing is sent. The MCP
  server has a tool for it. See [Conditions](/model#conditions).
- **Report templates and bulk reports.** `GET`, `POST /v1/report-templates` and `GET`, `PATCH`,
  `DELETE /v1/report-templates/{id}` read and write the brokerage's report builder templates.
  `POST /v1/bulk-report-runs` makes one PDF report for each deal from a template or a layout, and
  `GET /v1/bulk-report-runs` and `GET /v1/bulk-report-runs/{id}` follow the runs.
  `GET /v1/bulk-report-runs/{id}/files` gives links to the PDFs and the ZIP file. Nothing is sent
  to a client. The MCP server has a tool for each, except delete and the file links. See
  [Reports and data](/reports-and-data).
- **Files that came in by email.** `GET /v1/deals/{id}/email-attachments` lists the files from a
  client's email that wait for the broker before they go on the deal file, with a `status` filter.
  `POST .../accept` puts one on the deal file as a document, and `POST .../dismiss` keeps it off.
  The MCP server has a tool for each. See [Files that came in by email](/files#files-that-came-in-by-email).
- **Addresses and phone numbers on an applicant.** `GET /v1/deals/{id}/applicants/{applicantId}` now
  returns `addresses`, the applicant's address history with dates and a mailing address, and
  `phones`, the numbers the lender sees. `POST /v1/deals/{id}/applicants/{applicantId}/addresses`
  adds an address, and `PATCH` and `DELETE` on `.../addresses/{addressId}` change or remove one. The
  same three work on `.../phones`. The MCP server has a tool for each, except delete. See
  [Edit the application one row at a time](/model#edit-the-application-one-row-at-a-time).
- **PEP declaration forms.** `GET /v1/pep-forms` and `GET /v1/pep-forms/{id}` read the
  politically exposed person (PEP) forms sent to clients: the status, and the client's answers.
  `POST /v1/pep-forms` asks the broker to email the form to a client: the response is `202` with a
  pending `pep_form.send` approval. The MCP server has a tool for each. See
  [Ask a client to declare PEP status](/client-asks#ask-a-client-to-declare-pep-status).
- **Lender commitments.** `GET /v1/deals/{id}/commitments` and
  `GET /v1/deals/{id}/commitments/{commitmentId}` return the terms BrokerPlus read from the lender's
  commitment PDF, with the broker's corrections and the cost of borrowing. `POST /v1/deals/{id}/commitments`
  reads a PDF, and `POST .../review` marks the terms as checked, with corrections. The MCP server
  can read commitments, but it cannot read a PDF or review one. See
  [Lender commitments](/model#lender-commitments).
- **Value history of a deal's property.** `GET /v1/deals/{id}/property-valuations` lists each
  valuation of the property the deal's mortgage is on, newest first: the home value trend on the
  client's equity report. The MCP server has a tool for it. See
  [Value history of the deal's property](/model#value-history-of-the-deals-property).
- **Split mortgages.** An opportunity returns `segments`: the parts of a split mortgage, such as a
  fixed portion beside a line of credit, with each part's balance, rate, term and lender. Rates are
  percent. `PATCH /v1/opportunities/{id}` takes `segments` and replaces every segment. See
  [Change what the report shows](/equity-reports#change-what-the-report-shows).
- **Assistant instructions.** `GET`, `POST /v1/agent-instructions` and `GET`, `PATCH`,
  `DELETE /v1/agent-instructions/{id}` read and write the standing instructions a broker gives their
  assistant, the same list as Assistant settings in BrokerPlus. Only an admin can write a brokerage
  instruction. The MCP server has a tool for each, except delete. See
  [Assistant instructions](/workspace#assistant-instructions).
- **E-signing rules.** `GET`, `POST /v1/signing-policies` and `DELETE /v1/signing-policies/{id}`
  read and set which connected e-signing provider a team's documents go out through: the default,
  and an exception per lender. A rule names the provider. The MCP server can list and set rules,
  but not delete one. See [E-signing rules](/compliance#e-signing-rules).
- **The compliance program.** `GET /v1/compliance/program` reads the brokerage's compliance program,
  and `PATCH` changes the facts about the brokerage that the borrower disclosure prints:
  whom it acts for, how many lenders it acted for last year, and whether it lends. The rest of
  the program changes only in BrokerPlus. `GET /v1/compliance/trainings`,
  `GET /v1/compliance/retention-policies`, `GET /v1/compliance/verifier-policy` and
  `GET /v1/compliance/monitoring-alerts` read the training records, how long records are kept, the
  document check, and the alerts that screening raised. The MCP server has a tool for each, except
  the alerts. See [The compliance program](/compliance#the-compliance-program).
- **A deal's compliance package.** `GET`, `POST`, `PATCH /v1/deals/{id}/compliance/package` read,
  make from a template, and change the one PDF that holds a deal's documents for a lender or an
  auditor. `POST /v1/deals/{id}/compliance/package/build` builds it, and is refused while a required
  document is missing or left out. `GET /v1/deals/{id}/compliance/package/download` answers a link
  to the PDF. The MCP server has a tool for each, except the download. See
  [A deal's package](/compliance#a-deals-package).
- **Compliance package templates.** `GET`, `POST /v1/compliance/package-templates` and `GET`,
  `PATCH`, `DELETE /v1/compliance/package-templates/{id}` read and change the layouts that a deal's
  compliance package is built from: a team's own templates, and the brokerage's, which only a
  compliance administrator changes. `GET /v1/compliance/package-requirements` lists what a slot can
  hold. The MCP server has a tool for each, except delete. See
  [Package templates](/compliance#package-templates).
- **Evidence for a custom requirement.** In `GET /v1/deals/{id}/compliance`, each of the
  `document_requirements` now has `custom_obligation_id` and `evidence`, the documents that answered
  it. `POST /v1/deals/{id}/compliance/custom-obligations/{obligationId}/documents` uploads a file
  for a custom requirement, and `DELETE` on `.../documents/{documentId}` takes one off. The MCP
  server can upload, but it cannot take a document off. See
  [Evidence for a custom requirement](/broker-view#evidence-for-a-custom-requirement).
- **Identity verifications.** `GET /v1/deals/{id}/identity-verifications` lists the identity
  verifications of the people on a deal, with the evidence each one examined. A document number
  shows only its last four characters, and the images and the reports are not published. The MCP
  server has no tool for it. See [Identity verifications](/broker-view#identity-verifications).
- **Deal risk assessment.** `GET` and `PATCH /v1/deals/{id}/risk-assessment` read and save the
  FINTRAC risk assessment of a deal: the transaction types, the fraud questions, the FINTRAC report,
  and the risk level. A save adds the answers you send to the answers on file. The MCP server has no
  tools for it. See [Deal risk assessment](/broker-view#deal-risk-assessment).
- **Know-your-client records.** `GET /v1/deals/{id}/kyc-records` and `GET`,
  `PATCH /v1/deals/{id}/kyc-records/{contactId}` read and save the FINTRAC information record of each
  party on a deal, as the KYC editor does. `GET /v1/deals/{id}/entity-verifications` and `GET`,
  `PATCH /v1/deals/{id}/entity-verifications/{contactId}` do the same for the entity verification of
  a company. An imported ID number shows only its last four characters. The MCP server has no tools
  for these records. See [Know-your-client records](/broker-view#know-your-client-records).
- **The changes asked of a client.** `GET /v1/intakes/{id}/change-requests` lists every answer and
  file the broker asked a client to fix, with a `status` filter. `PATCH` rewords one,
  `POST .../resolve` and `POST .../reopen` mark it done or open again, and `DELETE` withdraws it.
  None of these sends anything. The MCP server has a tool for each, except delete. See
  [Follow up on the changes you asked for](/intake#follow-up-on-the-changes-you-asked-for).
- **Strategy analyses.** `GET`, `POST /v1/strategy-analyses` and `GET`, `PATCH`,
  `DELETE /v1/strategy-analyses/{id}` read and write the strategy board's analyses: up to 8 mortgage
  options compared against one, with the client's plans and the note that explains the
  recommendation. Rates are percent. The MCP server has a tool for each, except delete. See
  [Strategy analyses](/planning#strategy-analyses).
- **Booked meetings are leads.** A lead's `source` and a contact's `created_via` take the new value
  `booking`: a person who booked a meeting through one of the broker's booking links. It fires
  `lead.created` like a form lead. A lead sent with `POST /v1/leads` now also lands on the broker's
  Where New Leads Go column, the same as a form lead.
- **Maturity date on the application.** `PATCH /v1/deals/{id}/application` takes `maturity_date`.
  This is the date that stage requirements check. The `maturity_date` on `PATCH /v1/deals/{id}` is a
  different field, and stage requirements do not read it.
- **Ground rent and the condo fee.** A subject property has `annual_ground_rent`, the yearly ground
  rent on leasehold land. When `condo_fee_tds_pct` is `null`, BrokerPlus counts all of the condo fee
  in the debt service ratios. Before this change, it counted half.
- **Secured lines of credit.** A liability has `secured_by_home`, `true` when the broker marked a line
  of credit as secured against a home. The debt service ratios count such a line at the higher of its
  stated payment and its balance repaid over 25 years at the qualifying rate.
- **How BrokerPlus calculates `gds` and `tds` changed.** These rules are new, and each one can change
  `gds` and `tds` on a deal that already has them:
  - A subject property with no property tax is charged the tax bill on file, when one matches its
    address as the application holds it now. Otherwise it is charged 1% a year of the purchase price or the appraisal, whichever is
    lower.
  - A subject property with no heating is charged the heating the client gave. Otherwise it is charged
    $100 a month, or $0.75 a square foot a year if that is higher.
  - Every other property that counts in `tds` and has no property tax is charged 1% of its value a
    year. One with no heating is charged $100 a month.
  - Each rental property's rent is offset or added back by its own stated treatment and percent.
    Before this change, the first rental in the list set the treatment for all of them.
  - An instalment or student loan that owes money and has no payment, or a $0 payment, is charged 3%
    of its balance a month.
  - A credit card or an unsecured line of credit is charged the higher of its stated payment, 3% of the
    balance and $10.
  - Child benefit counts as at most 30% of the qualifying income.
  - Ground rent counts in full.
  A zero tax, heating or payment counts as blank in each of these rules.

## 2026-10-09

- **Files on snippets.** `POST /v1/snippets/{id}/attachments` adds a file to a snippet, and
  `DELETE /v1/snippets/{id}/attachments/{attachmentId}` removes one. The file's type is read from its
  content, and content with no recognisable type is taken only as CSV. The MCP server has a tool to add
  a file, but not to remove one. See [Snippets](/mail#snippets).
- **What the checks raised on an intake.** `GET /v1/intakes/{id}/flags` lists the flags BrokerPlus's
  checks raised on a client's intake, the most serious first, with a `status` filter.
  `POST /v1/intakes/{id}/flags/{flagId}/override` sets one aside with the broker's reason, as
  **Override** does in BrokerPlus. The MCP server has a tool for both. See
  [Review what the checks raised](/intake#review-what-the-checks-raised).
- **Participant seats.** `GET`, `POST`, `PATCH`, and `DELETE` on `/v1/participant-seats` read, add,
  pin, and remove the seats that a team's deals keep for a participant, with a default person for
  each. `GET /v1/deals/{id}/participant-seats` shows which seats one deal has, and
  `POST /v1/deals/{id}/participant-seats/{seatId}/adopt` and `.../dismiss` change that deal only.
  The MCP server has a tool for each read and for adopt and dismiss. See
  [Participant seats](/workspace#participant-seats).
- **Offers and scenarios on a deal.** `GET`, `POST /v1/deals/{id}/offers` and `GET`, `PATCH`,
  `DELETE /v1/deals/{id}/offers/{offerId}` read and write the lender offers on a deal's offers tab,
  with their fees. `GET /v1/deals/{id}/offers/compare` is the tab's side-by-side comparison: payment,
  insurance, penalty, fees and APR for each offer, and each scenario priced at its rates.
  `/v1/deals/{id}/scenarios` reads and writes the deal's what-if scenarios the same way. Rates are
  percent. The MCP server has a tool for each, except delete. See
  [Offers and the comparison](/planning#offers-and-the-comparison).
- **Lead forms.** `GET`, `POST`, `PATCH`, and `DELETE` on `/v1/lead-forms` build, edit, and delete the
  forms a broker puts on a website, with the whole form in `config`, its page in `url`, and how many
  leads it brought in. `GET /v1/leads` takes `lead_form_id` to list one form's leads. A write cannot
  turn on a form's auto-reply: `POST /v1/lead-forms/{id}/turn-on-auto-reply` asks the broker, who
  approves it in BrokerPlus. `GET /v1/approvals` has the new action `lead_form.auto_reply` and the
  target type `lead_form`. The MCP server has a tool for each, except delete. See
  [Lead forms](/capture-leads#lead-forms).
- **Who referred whom.** `GET`, `POST /v1/contact-referrals` and `GET`, `PATCH`,
  `DELETE /v1/contact-referrals/{id}` read and write the referral between two contacts: the person
  who sent a client to the broker, with its kind, status, note, and linked deal. The MCP server can
  list, read, record, and edit them. See [Who referred whom](/workspace#who-referred-whom).
- **Stop automations messaging a client.** `POST /v1/automations/stops` stops automations from
  messaging a client, for every deal or one, and for every kind of automation or some. It happens now.
  `GET /v1/automations/stops` lists the stops. `POST /v1/automations/stops/{id}/lift` asks the broker
  to lift one, with the new approval action `automation_stop.lift`, and the new approval target type
  `automation_stop`. The MCP server has a tool for each. See
  [Stop automations messaging a client](/automations#stop-automations-messaging-a-client).
- **Automation canvas groups.** `GET`, `POST`, `PATCH`, and `DELETE /v1/automations/canvases` read
  and change the groups that automations are filed under on the automation canvas.
  `POST /v1/automations/canvases/{id}/turn-off` turns a group off now.
  `POST /v1/automations/canvases/{id}/turn-on` turns a group on: an automation that can message
  clients waits for the broker's approval, the same as one turned on by itself. The MCP server has a
  tool for each operation except the delete. See [Group automations on the canvas](/automations#group-automations-on-the-canvas).
- **Call lists.** `/v1/call-lists` makes, renames, and deletes the broker's call lists, from contacts,
  deals, or rows of names and numbers (`POST /v1/call-lists/import`). `/v1/call-lists/{id}/entries`
  lists the people on a list, adds more, records each call's outcome and note, and takes a person off.
  Nothing is sent to anybody. The MCP server has a tool for each, except the deletes. See
  [Call lists](/log-calls#call-lists).
- **Mentions, reactions, and files on notes.** A deal note and a contact note now have `mentions`,
  `reactions`, and `attachments`. `GET /v1/deals/{id}/notes/mentionable` lists who a note can
  mention. `POST /v1/deals/{id}/notes/{noteId}/reactions` adds or takes off an emoji reaction.
  `POST /v1/deals/{id}/notes/{noteId}/attachments` attaches a PDF, and
  `GET /v1/deals/{id}/notes/{noteId}/attachments/{attachmentId}` gives a link to it. The same paths
  work under `/v1/contacts`. The MCP server has a tool for each, except the download link. See
  [Mentions, reactions, and files](/model#mentions-reactions-and-files).
- **Debt consolidation analysis.** `POST /v1/deals/{id}/consolidation-analyses` compares paying out
  the client's debts with a refinance, a line of credit, or a reverse mortgage, the same as the Debts
  tab. `GET /v1/deals/{id}/consolidation-analyses` and
  `GET /v1/deals/{id}/consolidation-analyses/{analysisId}` read the saved analyses. A debt that you add
  or change through `/v1/deals/{id}/liabilities` now takes `debt_type`, which puts it in the analysis.
  The MCP server has a tool for each. See [Debt consolidation analysis](/model#debt-consolidation-analysis).
- **Who a deal is waiting on.** `GET /v1/work-watches` and `GET /v1/work-watches/{id}` read what each
  deal is waiting for, who owes the next move, since when, and how overdue it is: the waiting-on
  chips on a deal card. They are read-only. The MCP server has a tool for each. See
  [Who a deal is waiting on](/broker-view#who-a-deal-is-waiting-on).
- **Notes on a document.** `GET` and `POST` on `/v1/documents/{id}/notes`, and `PATCH` and `DELETE` on
  `/v1/documents/{id}/notes/{noteId}`, read and write the notes brokers keep on one deal document. Only the
  author changes or deletes a note. The MCP server has a tool for each, except delete. See
  [Notes on a document](/files#notes-on-a-document).
- **PDFs in the client hub.** `GET`, `POST` and `DELETE` on `/v1/hub-resources` list, add and remove
  the PDFs on the Resources tab of a client's hub, for every client or for one deal's client. The MCP
  server can list them. See [PDFs in the client hub](/files#pdfs-in-the-client-hub).
- **The team's own document types.** `GET`, `POST` and `DELETE` on `/v1/custom-document-types` read,
  add and remove the documents a team asks for that BrokerPlus does not know, each with the `custom:`
  code a document package names. Nothing reaches a client. The MCP server has a tool for each, except
  remove. See [The team's own document types](/files#the-teams-own-document-types).
- **Equity report send and open history.** `GET /v1/equity-reports/sends` lists each time a report
  went out to a client, with its channel, cadence, and whether it left BrokerPlus.
  `GET /v1/equity-reports/interactions` lists what clients did with their reports: opened the email,
  opened the report, asked for a call, or corrected a figure. Both take `updated_since` and
  `after_id`. The MCP server has a tool for each. See
  [Read the send and open history](/equity-reports#read-the-send-and-open-history).
- **Valuation history of an other property.** `GET /v1/deals/{id}/other-properties/{propertyId}/valuations`
  lists each valuation of the property, newest first, and the change in its estimate over the last
  year as a percent. `POST /v1/deals/{id}/other-properties/{propertyId}/refresh-valuation` asks for a
  new estimate in the background. The MCP server has a tool for each. See
  [Valuation history of an other property](/model#valuation-history-of-an-other-property).
- **Call scripts.** `GET`, `POST`, `PATCH`, and `DELETE` on `/v1/call-scripts` read, make, change, and
  retire the broker's own call scripts, the questions they read on a call. The MCP server has a tool for
  each, except retire. See [Call scripts](/log-calls#call-scripts).
- **Note types.** `GET /v1/note-types` lists the kinds that a deal note or a contact note can have.
  `POST /v1/note-types`, `PATCH /v1/note-types/{scope}/{slug}` and `DELETE /v1/note-types/{scope}/{slug}`
  add, change and remove kinds for the broker's team. `POST /v1/deals/{id}/notes` and
  `POST /v1/contacts/{id}/notes` accept `type`, so a new note can have a kind other than `general`.
  The MCP server has a tool for each. See [Note types](/workspace#note-types).
- **The broker's own rates.** `GET` and `POST /v1/me/rate-overrides` read and set the rates a broker
  keeps on the Rates page, in percent. `DELETE /v1/me/rate-overrides/{id}` removes one, and
  `POST /v1/me/rate-overrides/reset` goes back to live rates. `POST /v1/me/custom-lenders` adds
  custom lenders with their rates, and `PATCH /v1/me/custom-lenders/{lenderSlug}` renames one.
  `/v1/me/lender-offsets` reads, sets, and removes per-lender offsets in basis points. The MCP server
  has a tool for each read and set. See [Rates](/rates).
- **Funding packages.** `GET /v1/deals/{id}/funding-package` returns the lender's funding forms that
  the broker is preparing on a deal. Add forms with `POST /v1/deals/{id}/funding-package/items` or
  `POST /v1/deals/{id}/funding-package/upload`, map their fields with
  `POST /v1/deals/{id}/funding-package/items/{item_id}/map`, and set the signer, fields and state
  with `PATCH /v1/deals/{id}/funding-package/items/{item_id}`. The broker sends the package in
  BrokerPlus. The MCP server has a tool for the reads, the add, the map and the update. See
  [Funding packages](/files#funding-packages).
- **Partner roles on a contact.** `GET`, `POST /v1/contacts/{id}/partner-roles` and `PATCH`,
  `DELETE /v1/contacts/{id}/partner-roles/{roleId}` list, add, edit, and remove what a contact does
  for the broker's clients, such as `realtor` or `lawyer`, with a title, website, and booking link.
  The MCP server can list, add, and edit them. See [Partner roles](/workspace#partner-roles).
- **Snippets.** `GET /v1/snippets` lists the saved replies of the broker your key acts as, and
  `POST`, `PATCH` and `DELETE` on `/v1/snippets` add, edit and delete them, the same as the snippet
  manager in the inbox. `GET /v1/snippets/{id}/attachments/{attachmentId}` gives a link to a
  snippet's file. The MCP server can list, read, add and edit them. See [Snippets](/mail#snippets).
- **Mail folders.** `GET /v1/communication-folders` lists the folders and labels of a connected
  mailbox. `POST /v1/communications/{id}/folders` files a whole conversation into a folder, and
  `DELETE /v1/communications/{id}/folders/{folderId}` takes it out; the move also happens in the
  broker's mail app. Each folder in a communication's `folders` now has its `id`. The MCP server has a
  tool for each, except taking a conversation out. See
  [File a conversation in a folder](/mail#file-a-conversation-in-a-folder).
- **Inbox tags.** `GET /v1/communication-tags` lists the brokerage's inbox tags, and `POST`, `PATCH`
  and `DELETE` on `/v1/communication-tags` add, rename and archive them.
  `POST /v1/communications/{id}/tags` puts a tag on a whole conversation, and
  `DELETE /v1/communications/{id}/tags/{tagId}` takes it off. A communication now lists its tags in
  `tags`, each with its `id`, `name` and `color`. The MCP server has a tool for each, except taking a
  tag off and archiving one. See [Tag a conversation](/mail#tag-a-conversation).
- **Mortgage plans.** `GET /v1/mortgage-plans`, `GET /v1/mortgage-plans/{id}`, `POST /v1/mortgage-plans`,
  `PATCH /v1/mortgage-plans/{id}`, and `DELETE /v1/mortgage-plans/{id}` read and build the plans
  from the plan builder, on a deal or a contact. Rates are percent. The MCP server has a tool for
  each, except delete. See [Plans and comparisons](/planning#mortgage-plans).
- **Team note.** `GET /v1/teams/{id}/note` reads the note that a team's members share on their
  overview, and `PATCH /v1/teams/{id}/note` replaces it. Only a member of the team can reach it. The
  MCP server has a tool for each. See [Team note](/workspace#team-note).
- **Document packages.** `GET`, `POST`, `PATCH` and `DELETE` on `/v1/doc-packages` read and change
  the named sets of documents that the broker's team asks for together. `GET /v1/doc-packages/samples`
  lists the situations BrokerPlus recognizes, and `POST /v1/doc-packages/samples/{key}` adds one as a
  package. `GET /v1/doc-packages/{id}` reads one. Nothing reaches a client. The MCP server has a tool for
  each, except delete. See
  [Document packages](/files#document-packages).
- **The client tracker.** `/v1/tracker-stages` lists, adds, changes, reorders, and removes the stages
  a client sees on their hub, and sets the note from the broker. `GET /v1/deals/{id}/tracker` shows
  one deal's tracker as the client sees it. `POST /v1/deals/{id}/tracker/{stageId}/reach` returns a
  pending `tracker.reach` approval: the client sees the stage only when the broker approves.
  `POST /v1/deals/{id}/tracker/{stageId}/retract` takes stages back down at once. The MCP server can
  read the tracker and ask to reach a stage. See [Client tracker](/workspace#client-tracker).
- **What a client was asked for, item by item.** `GET /v1/intakes/{id}/items` lists each document,
  form, consent and identity check on the client's portal, with each applicant's state and the files
  they sent. `GET /v1/intakes/{id}/uploads` and `GET /v1/intakes/{id}/uploads/{uploadId}` read the
  files. `POST /v1/intakes/{id}/uploads/{uploadId}/stage` and `POST /v1/intakes/{id}/items/{key}/stage`
  accept a file or an item, as a move on the document board does. `POST /v1/intakes/{id}/request-changes`
  now takes `uploads` to refuse a file and ask the client for a new copy, and `changes` is optional.
  See [Read what the client was asked for](/intake#read-what-the-client-was-asked-for).

- **Request templates.** `GET /v1/request-templates` lists the broker's request library, and
  `GET /v1/request-templates/{id}` reads one template with its items. You can create, change, copy,
  star, reset and delete a template, and add, change, order, reset and remove its items. Building a
  template sends nothing. The MCP server has a tool for each, except the two deletes. See
  [Request templates](/client-asks#request-templates).
- **Ask for documents from a template.** `POST /v1/deals/{id}/document-requests` accepts
  `template_id`, and asks for the documents on that request template. `documents` is now optional
  when you send `template_id`. See [Ask from a request template](/client-asks#ask-from-a-request-template).
- **The brokerage's rates.** `/v1/brokerage/rate-overrides` and `/v1/brokerage/lender-offsets` read,
  set, and remove the rates and offsets a brokerage sets for every broker, in percent and basis
  points. `POST /v1/brokerage/rate-overrides/reset` goes back to live rates. Only a brokerage admin's
  key can call them. The MCP server has a tool for each read and set. See
  [The brokerage's rates](/rates#the-brokerages-rates).
- **Lender cashback.** `GET /v1/lender-cashback` lists the cashback each lender pays, in percent, as
  the opportunity figures use it. `POST /v1/lender-cashback` and `DELETE /v1/lender-cashback/{lenderSlug}`
  set and remove the brokerage's own, with a brokerage admin's key. The MCP server has a tool to read
  and to set. See [Lender cashback](/rates#lender-cashback).
- **Rate sheets.** `POST /v1/rate-sheets` uploads a lender's rate sheet and answers with the rates
  BrokerPlus read from it, in percent, and the cells they fit. `POST /v1/rate-sheets/{id}/apply` puts
  the rows you choose on the broker's own rates. `GET` lists and reads sheets, and `DELETE` removes
  one. The MCP server has a tool for each, except delete. See [Rate sheets](/rates#rate-sheets).
- **Partner hub links.** `POST /v1/partner-hubs` opens a referral partner's hub and issues a link that
  the partner opens to see every file they are on with the broker. `POST /v1/partner-hubs/{id}/link`
  issues another link to a hub. `GET /v1/partner-hubs` and `GET /v1/partner-hubs/{id}` list the hubs
  and their working links. BrokerPlus sends nothing: you give the link to the partner. The MCP server
  has a tool for each. See [Partner hub links](/workspace#partner-hub-links).
- **NPS alert rules.** `GET`, `POST`, `PATCH`, and `DELETE` on `/v1/nps-alert-rules` read and change
  the rules that alert brokers and add a task when a client answers a survey. The MCP server has a
  tool for each, except delete. See [NPS alert rules](/workspace#nps-alert-rules).
- **NPS surveys.** `GET /v1/nps-surveys` and `GET /v1/nps-surveys/{id}` read the surveys sent to
  clients, with each score, its band, and the comment. `POST /v1/nps-surveys` asks the broker to
  email a client the survey; it waits for the broker's approval, under the new approval action
  `nps.send`. The MCP server has a tool for each. See
  [Ask a client for a score](/client-asks#ask-a-client-for-a-score).
- **Choose the finished files on a partner hub.** A partner hub has `show_funded` and
  `show_not_proceeding`. They choose if the hub lists the files that funded and the files that ended
  without funding. Both are `true` by default. `POST /v1/partner-hubs` accepts either one.

## 2026-10-08

- **Record a funding date on a closed deal.** `PATCH /v1/deals/{id}` accepts `funded_at` on a deal
  that is already `closed`. The status stays `closed`. An imported file that its source system marked
  funded takes the date as is. On any other closed deal, the date counts the deal as funded, so it
  needs the same anti-money laundering (AML) clearance as a change to `funded`. You can't close a deal
  and date it in one request.
- **Write accelerated payments.** `PATCH /v1/deals/{id}/application` now accepts `payment_accelerated`:
  `true`, `false`, or `null`. Pair it with `payment_frequency: 26` for bi-weekly or `52` for weekly.
- **Invalid numeric codes name the choices.** Validation errors now list each accepted code and
  label. The API reference also lists the accepted numeric codes. See [Units and formats](/units).

- **You can cancel an approval you asked for.** `POST /v1/approvals/{id}/cancel` withdraws a
  `pending` approval before the broker answers. It becomes `rejected` with the reason
  `Cancelled by the requester.` Only the key that asked can cancel it.
- **Asking twice files one approval.** A repeat of a request that is still `pending` (same action,
  record, recipients, sender and words, from the same person) returns the existing approval instead
  of a second card for the broker.
- **An approval whose record is deleted is voided.** Deleting the campaign, appointment, automation,
  automation run or deal an approval is about now voids it at once, with `"changed": ["missing"]`,
  instead of leaving it `pending`.
- **Registering the same webhook twice is refused.** `POST /v1/webhooks` with a URL and events you
  already registered returns `409` `WEBHOOK_EXISTS`, with the existing endpoint in
  `details.webhook_id`. The same URL for different events, or another broker's endpoint on it, is
  still allowed.
- **Every authenticated response carries `Cache-Control: no-store`**, so no proxy or HTTP cache keeps
  a broker's data.
- **The MCP server refuses an argument a tool does not take.** It used to drop it, so a misspelled
  filter returned every row. The error names the argument and lists the ones the tool takes.
- **An unknown filter value is refused.** `status` on deals and on a deal's conditions, `type` on
  automations, and `include_archived` on templates now return `400` `VALIDATION_ERROR` with the
  values they take. A value such as `status=BOGUS` used to return an empty page. The filters on
  approvals, automation runs, custom fields, and `archive_deals` on a pipeline delete now name the
  parameter in `details.fieldErrors` too.
- **`after_id` works on every list that documents it.** Templates, automations, meeting types, a
  deal's documents, and a deal's terms history now resume a sync after the row you name, and return
  `400` `VALIDATION_ERROR` when `after_id` comes without `updated_since`. They used to ignore it.
  `GET /v1/contacts/duplicates` refuses `after_id`, because a set has no position to resume after.
- **Fixed: an equity report's `ltv` could read 100 times too high.** Some reports returned `5170`
  where the loan-to-value was `51.7`. Every report now returns its `ltv` as a percent.
- **A control character in a query value is refused.** A NUL or another control character in any
  query parameter, such as `lender_file_ref=%00`, now returns `400` `VALIDATION_ERROR` naming the
  parameter. It used to return `500`. Tabs and line breaks are still accepted.
- **A `page` out of range is refused.** Every list takes `page` from `1` to `1000000`. A larger
  `page`, or a `page` or `page_size` that is not a whole number, now returns `400`
  `VALIDATION_ERROR` naming the parameter. A very large `page` used to return `500`, and a `page`
  such as `abc` was read as page `1`. See [Pagination](/pagination).
- **A date that does not exist is refused.** `updated_since`, `from` and `to` on availability, and a
  report's `start` and `end` now return `400` `VALIDATION_ERROR` for a date such as `2026-02-30`
  or a time such as `24:00`. These used to be read as the next real date, so
  `updated_since=2026-02-30T00:00:00Z` quietly meant March 2.
- **`GET /v1/changes` refuses a checkpoint sent with more `types` than it was issued for.** A
  checkpoint moves past every entry its own `types` covered, so reading more types from it skipped
  their entries without a word. It now returns `400 INVALID_CHECKPOINT`. The same `types`, or
  fewer, still work. A checkpoint from `after=now` belongs to the `types` sent with it, or to
  every type when none were sent. A `types` value that names no type, such as `,,,`, now returns
  `400` instead of `500`, and so does a malformed checkpoint. See [Sync](/sync).
- **Breaking: a referral partner's `conversion_rate` is now a percent.** It was a decimal fraction,
  such as `0.25`. It is now `25`, like every other rate. This is on `GET /v1/referral-partners`
  and `GET /v1/referral-partners/{id}`. See [Units and formats](/units).

## 2026-10-07

**Summary: the API grew to about 300 operations, with three breaking changes.**

If you built on the API before today, check these three first:

1. **Rates on an opportunity, and the `ltv` on an equity report, are now percents.**
   `target_new_rate`, `breakeven_rate`, and `ltv` were decimal fractions, such as `0.0499`. They are
   now `4.99`. Remove any step that multiplies them by 100. See [Units and formats](/units).
2. **Booking, moving, or cancelling a meeting, asking for intake changes, and emailing an intake now
   wait for the broker's approval.** `POST /v1/appointments/book`, `.../reschedule`, `.../cancel`,
   and `POST /v1/intakes/{id}/request-changes` return `202` and a pending approval instead of acting
   at once. `POST /v1/intakes` with `delivery: "email"` still returns `201`, with an `approval_id`,
   and emails nobody until the broker approves. See [Approvals](/approvals).
3. **The action on many tasks moved to `POST /v1/tasks/bulk`.** `POST /v1/tasks/batch` now creates or
   updates tasks, like the other batch endpoints.

What is new, in short: batch writes for contacts, leads, deals, and tasks; the mortgage application
one row at a time; document uploads, requests, generated letters, and signing; workspace setup
(custom fields, tags, pipelines, lead stages, task categories, meeting types, hours); automations
and templates; equity report enrollment; the call log; suggestions, notifications, compliance and
credit reads, and activity trails; sandboxes; key limits; and sign-in for MCP connectors. The
entries below have the detail.

These docs were reorganized around the same release: a home page by job, one
[Concepts](/authentication) section, and the old Claude command center page merged into
[Run an AI assistant](/mcp). Old links still work.

**Added: flags on a deal.**

- `GET /v1/deals/{id}/flags` lists the flags that are up on a deal, with each reason in plain words.
- `POST /v1/deals/{id}/flags` raises the broker's flag, and `POST /v1/deals/{id}/flags/{flagId}/clear` takes it down. A flag from an automation or from BrokerPlus is read-only. Refer to [Flags](/model#flags).

**Changed: an approval task cannot be cancelled or reopened with the API.**

- `POST /v1/tasks/{id}/cancel` and `POST /v1/tasks/{id}/reopen` now answer `409` with `APPROVAL_TASK` for an approval task, as `POST /v1/tasks/{id}/complete` already did. Only the broker decides an approval, in BrokerPlus.

**Added: read and set the checklist on a deal.**

- `GET /v1/deals/{id}/checkpoints` lists the steps of the checklist and their values on the deal. `PATCH /v1/deals/{id}/checkpoints/{checkpointId}` sets one step. Refer to [The deal's checklist](/model#the-deals-checklist).

**Changed: the action on many tasks moved to `POST /v1/tasks/bulk`.**

- It was at `POST /v1/tasks/batch` for a few hours today. `POST /v1/tasks/batch` now creates or updates tasks, as it does for contacts, leads, and deals.

**Added: copy a deal, and restore an archived one.**

- `POST /v1/deals/{id}/duplicate` makes a new deal with the same people. `POST /v1/deals/{id}/restore` puts an archived deal back on its pipeline. Refer to [Copy, archive, and restore a deal](/model#copy-archive-and-restore-a-deal).

**Added: batch writes for contacts, leads, deals, and tasks.**

- `POST /v1/contacts/batch`, `POST /v1/leads/batch`, `POST /v1/deals/batch`, and `POST /v1/tasks/batch` create or update up to 100 records in one request. Refer to [Migrate from another CRM](/migrate-from-a-crm#send-records-in-batches).
- Send `external_id` with `namespace`, and a later batch with the same id updates the same record.
- An item that fails does not stop the other items. Each item counts as one request against the rate limit.
- The MCP server does not have batch tools. An assistant writes one record at a time.

**Added: complete, cancel, or reopen many tasks at once.**

- `POST /v1/tasks/bulk` applies one action to up to 200 tasks and lists the tasks it changed and skipped. Refer to [Tasks](/model#tasks).

**Added: the call log, and calls from other phone systems.**

- `POST /v1/calls` puts a call from another phone system or recorder in the call log of the broker, with its transcript. The call shows on the contact and the deal. Refer to [Log calls from your phone system](/log-calls).
- `GET /v1/calls` and `GET /v1/calls/{id}` read the call log, with each transcript and recap.
- The `call.logged` webhook event fires when a call lands in the call log.
- The MCP server has the `brokerplus_log_call`, `brokerplus_list_calls`, and `brokerplus_get_call` tools.

**Added: send documents to sign.**

- `GET /v1/deals/{id}/signing-documents` and `GET /v1/deals/{id}/signing-documents/{documentId}` read a deal's signing package, with where each signing is in `status`.
- `GET /v1/deals/{id}/signing-presets` lists the presets for a deal, and `POST /v1/deals/{id}/signing-documents` adds a saved document, or each one in a preset, for the person who signs it.
- `POST /v1/deals/{id}/signing-documents/{documentId}/preview` returns the document filled in, as a PDF.
- `POST /v1/deals/{id}/signing-documents/send` returns a pending approval with the new action `signing.send`. When the broker approves, each document is filled in and checked again, and each signer gets one email. `POST /v1/deals/{id}/signing-documents/{documentId}/resend` returns a pending approval with the new action `signing.resend`. A key with the `no_sending` limit cannot send or resend. Refer to [Ask a client for something](/client-asks).

**Added: change, copy, pause, and delete campaigns and audiences.**

- `PATCH /v1/campaigns/{id}` changes a draft. `POST /v1/campaigns/{id}/duplicate` copies a campaign as a new draft. `POST /v1/campaigns/{id}/pause` stops a campaign that is sending. `DELETE /v1/campaigns/{id}` deletes a campaign.
- `PATCH /v1/audiences/{id}` and `DELETE /v1/audiences/{id}` change and delete an audience. Refer to [Approvals](/approvals#campaigns).

**Added: act on a conversation in the inbox.**

- `PATCH /v1/communications/{id}` marks a conversation read or unread, marks it done, stars it, sets or clears a reminder, and files its mail on a deal. Refer to [Mail](/mail).

**Added: ask a client for documents, and explain conditions to the people on a deal.**

- `POST /v1/deals/{id}/document-requests` asks the client for documents, as the Request Documents window does. It returns `202` and a pending approval with the new action `document_request.send`. When the broker approves, the documents are added to the client's portal and each applicant gets an email.
- `POST /v1/deals/{id}/condition-requests` writes the letter that explains conditions in plain language, files it as the broker's draft, and returns `202` and a pending `message.send` approval.
- A key with the `no_sending` limit cannot make these requests. Refer to [Ask a client for something](/client-asks).

**Added: generated documents.**

- `GET /v1/document-templates` lists the document templates the key's user can use, for example an approval letter.
- `POST /v1/deals/{id}/generated-documents` makes a document from a template and files the PDF on the deal. If the broker must fix something first, the document stays a `draft` and `issues` says what.
- `GET /v1/deals/{id}/generated-documents` and `GET /v1/deals/{id}/generated-documents/{documentId}` read them.
- `POST /v1/deals/{id}/generated-documents/{documentId}/send` files an email to one person on the deal with the PDF attached, and returns `202` and a pending `message.send` approval. A key with the `no_sending` limit cannot send one, and a key with the `no_documents` limit cannot use any of these.

**Added: rename, refile, verify, and delete documents.**

- `PATCH /v1/documents/{id}` changes `display_name`, `contact_id`, and `excluded_from_submission`.
- `POST /v1/documents/{id}/stage` moves a document between `pending`, `review`, and `verified`, and files a verified document in a `folder`.
- `DELETE /v1/documents/{id}` deletes a document. Refer to [Files](/files).

**Added: change the tags, lead stages, lead channels, and referral partners of a brokerage.**

- `POST /v1/contact-tags` and `POST /v1/deal-tags` create a tag. `PATCH` changes its name or colour, and `DELETE` archives it and takes it off every record.
- `POST /v1/lead-stages` creates a column on the Leads board, `PATCH` renames it, and `POST /v1/lead-stages/reorder` sets the order. `DELETE /v1/lead-stages/{id}` moves the stage's leads to `move_to`, or to the first stage that stays.
- `POST`, `PATCH`, and `DELETE` on `/v1/lead-channels` create, change, and archive a lead channel.
- `POST /v1/referral-sources` creates a referral partner and its partner contact. `PATCH` changes it, and `DELETE` archives it.
- `GET /v1/lead-stages` and `GET /v1/lead-channels` now return the default stages and channels to a brokerage that did not open the Leads board yet.
- A key with the `leads_only` limit cannot change the lead stages, lead channels, or referral partners. Refer to [Configure your workspace](/workspace).

**Added: suggestions, notifications, compliance checklists, credit reports, and activity trails.**

- `GET /v1/suggestions` lists the changes that BrokerPlus suggests to the broker. `POST /v1/suggestions/{id}/approve` carries out a suggestion that stays inside BrokerPlus, and `POST /v1/suggestions/{id}/dismiss` turns one down. A suggestion that reaches a client returns `409 APPROVE_IN_BROKERPLUS`: the broker approves it in BrokerPlus. Refer to [Suggestions, notifications, and deal checks](/broker-view).
- `GET /v1/notifications` lists the notifications of the broker that your key acts as. `POST /v1/notifications/{id}/read` and `POST /v1/notifications/read-all` mark them read.
- `GET /v1/deals/{id}/compliance` returns the compliance checklist of the deal: the checkpoints, the open items, the identity and know-your-client checks on each party, the consents, the suitability answers, and the required documents. It does not contain ID numbers, ID images, or dates of birth.
- `GET /v1/deals/{id}/credit-reports` returns the score, the bureau, the report date, and the account totals of each credit report. It does not contain the report or the account numbers. The API cannot pull credit.
- `GET /v1/deals/{id}/activity` and `GET /v1/contacts/{id}/activity` return the activity trail: messages, calls, notes, and changes to the deal.
- The MCP server has tools for suggestions, notifications, and activity trails.

**Added: change meeting types, the broker's hours, blocked days, and the email signature.**

- `POST /v1/meeting-types` creates a meeting type. `PATCH /v1/meeting-types/{id}` changes its length, hours, buffers, notice, questions, form fields, and reminders. `DELETE /v1/meeting-types/{id}` deletes it. `POST /v1/meeting-types/reorder` sets their order.
- `GET /v1/me/hours` and `PATCH /v1/me/hours` read and change the time zone, phone hours, and booking hours of the key's broker.
- `GET /v1/me/blocked-days`, `POST /v1/me/blocked-days`, and `DELETE /v1/me/blocked-days/{date}` list, block, and unblock the days when clients cannot book the broker.
- `GET /v1/me/email-signature` and `PATCH /v1/me/email-signature` read and change the broker's email signature.
- A key changes only its own broker's hours and signature. Refer to [Configure your workspace](/workspace).

**Added: edit, pin, and delete notes.**

- `PATCH /v1/contacts/{id}/notes/{noteId}` and `PATCH /v1/deals/{id}/notes/{noteId}` change the words, the pin, or the kind of a note.
- `DELETE /v1/contacts/{id}/notes/{noteId}` and `DELETE /v1/deals/{id}/notes/{noteId}` delete a note and its replies.
- `PATCH /v1/tasks/{id}/notes/{noteId}` changes the words or the pin of a task note. `DELETE /v1/tasks/{id}/notes/{noteId}` deletes a task note that you wrote.

**Added: change the task categories of a team.**

- `POST /v1/task-categories` creates a category, `PATCH /v1/task-categories/{id}` renames it, and `DELETE /v1/task-categories/{id}` deletes it. Its tasks go back to their built-in category.
- `POST /v1/task-categories/hidden` hides built-in categories from the team. `GET /v1/task-categories` now returns `hidden` on each category.
- Only the team's broker or a brokerage admin can make these changes. Refer to [Configure your workspace](/workspace).

**Added: change the pipelines and stages of a team.**

- `POST /v1/pipelines` creates a pipeline. `PATCH /v1/pipelines/{id}` changes it, `POST /v1/pipelines/reorder` sets the order, and `DELETE /v1/pipelines/{id}` deletes it. A pipeline that has deals is deleted only with `archive_deals=true`.
- `POST /v1/pipelines/{id}/stages`, `PATCH /v1/pipelines/{id}/stages/{stage_id}`, `POST /v1/pipelines/{id}/stages/reorder`, and `DELETE /v1/pipelines/{id}/stages/{stage_id}` add, change, order, and archive its stages.
- Each stage returns `requirements`: what a deal must have before it can move into the stage. You can set them on a create or an update. Refer to [Configure your workspace](/workspace).

**Added: change the custom fields of a brokerage.**

- `POST /v1/custom-fields` creates a contact or deal field. `PATCH /v1/custom-fields/{id}` changes its label, options, group, or default. `DELETE /v1/custom-fields/{id}` archives it, and BrokerPlus keeps its answers.
- `POST /v1/custom-fields/reorder` sets the order of the fields.
- Only a key that belongs to a brokerage admin can make these changes. Refer to [Configure your workspace](/workspace).

**Added: a common-law relationship type.**

- `GET /v1/contacts/{id}/relationships` can return `common_law` as a `type`. It reads the same from both contacts.

**Added: equity report enrollment, and opportunities you add by hand.**

- `POST /v1/equity-reports/enroll` asks the broker to start a client's recurring equity reports. `POST /v1/equity-reports/resume` asks to restart paused ones. `POST /v1/equity-reports/send` asks to send one report now, by email or SMS. Each returns `202` with a pending approval and a line for each deal. Nothing starts or goes out until the broker approves it in BrokerPlus. Refer to [Equity reports](/equity-reports).
- `POST /v1/equity-reports/pause`, `POST /v1/equity-reports/unenroll`, and `POST /v1/equity-reports/frequency` pause reports, stop them, or change how often they go out. They happen immediately.
- A client who opted out of reports, or is under a sanctions freeze, is refused for each deal, the same as in BrokerPlus.
- `POST /v1/opportunities` adds a mortgage to the book, the same as Add Client in BrokerPlus.
- `PATCH /v1/opportunities/{id}` takes `name`.
- `GET /v1/opportunities` takes `monthly_reports_enabled`.
- An approval can have the action `equity_report.enroll`, `equity_report.resume`, or `equity_report.send`, and the target type `deal`.

**Added: build and run the same automations as the Pulse builder.**

- `GET /v1/automations/catalog` lists every trigger type and its `config`, every step kind and its `params`, who a message can go to, the fields a check can compare, and the node kinds of a graph.
- `POST /v1/automations` and `PATCH /v1/automations/{id}` take any trigger `type` with `config`, `filters`, `repeat_policy`, `anchor_offset_days`, and `canvas_id`, and any step `kind` with `params` and `only_if`. Send `graph` for an automation with branches and waits. The older body (`stage_id`, and steps with `channel` and `template_id`) still works.
- `POST /v1/automations/{id}/turn-on` turns an automation on. When it can message clients, the response is `202` and an approval with the new action `automation.turn_on`, bound to the automation as it is written. A change before the broker decides voids it. `POST /v1/automations/{id}/turn-off` turns it off now.
- `POST /v1/automations/{id}/duplicate` makes a copy, OFF. `DELETE /v1/automations/{id}` turns an automation off and removes it from the board.
- Each automation returns `can_message_clients` and `graph`.
- `GET /v1/automation-runs` takes `deal_id` and `contact_id`.
- A refused automation returns `details.fieldErrors` with the field at fault, for example `steps.1.params.templateId`, and a message that says how to fix it.

**Changed: what turns an automation OFF.**

- A `PATCH` that changes only `name`, `description`, or `canvas_id` now leaves the automation on or off. Before, every change turned it OFF. A change to what the automation does still turns it OFF.
- A step `kind` other than `email` or `sms`, and a trigger other than `stage.transitioned`, no longer return `409 UNSUPPORTED_STEPS` or `409 UNSUPPORTED_AUTOMATION`.

**Added: who recorded a contact relationship, and when it last changed.**

- `GET /v1/contacts/{id}/relationships` returns `created_by_user_id` and `updated_at` on each relationship.

**Added: more fields on keys, brokers, teams, webhooks, automations, templates, meeting types, appointments, mail, drafts, campaigns, and intakes.**

- `GET /v1/me` returns `api_key.created_at`.
- `GET /v1/organization` returns `has_book_of_business`.
- `GET /v1/users` returns `timezone` and `license_number` for each broker.
- `GET /v1/teams/{id}/members` returns `created_at` and `updated_at` for each seat.
- `GET /v1/webhooks` returns `failure_count`, the events in a row that failed, and `updated_at`.
- `GET /v1/automations` returns `sort_order` and `anchor_offset_days`, and `created_at` and `updated_at` on each step.
- `GET /v1/templates` returns `attachments`: the name, size, and type of each file that goes with an email.
- `GET /v1/meeting-types` returns `buffer_before_minutes`, `buffer_after_minutes`, `slot_interval_minutes`, `availability_hours`, `required_license_class`, `required_province`, `reminder_minutes`, `reminder_channels`, and `position`.
- `GET /v1/appointments` returns `ical_uid` and `shows_as_free`.
- `GET /v1/communications` returns `to_contact_ids`, `mailbox_user_id`, `done_at`, and `remind_at`.
- `GET /v1/drafts` returns `body_html`, `reviewed_by_user_id`, and `rejection_reason`.
- `GET /v1/campaigns` returns `series_id`, `follow_up_root_id`, `follow_up_step`, `follow_up_audience`, `started_at`, `paused_at`, `auto_paused_reason`, and `failure_reason`. Each recipient has `updated_at`.
- `GET /v1/intakes` returns `portal_type`, `broker_user_id`, `template_id`, `application_id`, `status_note`, `status_note_updated_at`, `needs_changes_at`, `last_reminder_at`, and `reminder_count`.

**Added: sandboxes for testing.**

- An admin can create a sandbox in **Settings > Developer**. A sandbox is a private brokerage with sample data. It records email, text messages, credit requests, and lender submissions, and it does not send them. Refer to [Test in a sandbox](/sandbox).
- A sandbox key starts with `bp_test_`. A sandbox key cannot get data from a live brokerage, and a live key cannot get data from a sandbox.
- `GET /v1/sandbox/outbox` lists what a sandbox recorded. Only a sandbox key can use it.

**Added: more of what a broker sees on deals, opportunities, submissions, lenders, pipelines and commission.**

- A deal has `stage_entered_at`, `application_date`, `archived_at`, `opportunity_type`, `origin`, `spawned_from_deal_id`, `realtor_contact_id`, `primary_application_id`, `submitting_broker_id`, and `channel_id`. Each entry in `external_references` has `url`, `remote_status`, `remote_status_at`, `last_pulled_at`, and `created_at`.
- An opportunity has `name`, `comparison_rate`, `collateral_charge`, `cashback_enabled`, `cashback_override`, `best_option_pin`, `dismissed_by_user_id`, `last_client_contact_at`, `client_perceived_value`, `client_perceived_value_at`, the property details (`property_bedrooms`, `property_bathrooms`, `property_sqft`, `property_year_built`, `property_type`), the report dates, `outreach_blocked_at`, and `report_frequency`. Rates are in percent.
- `PATCH /v1/opportunities/{id}` also takes `comparison_rate`, `collateral_charge`, `cashback_enabled`, `cashback_override`, and the five property details. A value you send replaces the figure BrokerPlus has.
- A submission has `cover_note`, `penalty_capitalized`, `simulated`, `validated_at`, and, on a Finmo submission, `goal`, `approval_type`, `gds`, `tds`, and `ltv` in percent. A `simulated` submission never reached a lender.
- A lender has `contact_email`, `contact_phone`, `equifax_accredited`, `transunion_accredited`, `cmhc_approved`, `under_maintenance`, and `lending_guidelines`.
- A pipeline has `key` and `broker_user_id`. A stage has `description`, `exclude_from_book`, `stops_automated_outreach`, `created_at`, and `updated_at`.
- A commission has `created_by_user_id`. Its line items have `created_at` and `updated_at`, and its splits have `created_at`.

**Changed: an intake that BrokerPlus emails now needs the broker's approval.**

- `POST /v1/intakes` with `delivery` `email` still creates the intake and returns `201`, but nobody is emailed until the broker approves it in BrokerPlus. The response has a new field, `approval_id`. Read the approval with `GET /v1/approvals/{id}`. For `delivery` `link`, `approval_id` is null and nothing changes.

**Added: apps connect to the MCP server by signing in.**

- Claude on the web, ChatGPT, and other connectors sign in with OAuth 2.1 and need no API key. The `401` from the MCP server names the protected resource metadata, and that names the authorization server. Refer to [MCP server](/mcp#sign-in-instead-of-a-key).
- The broker picks the team and whether the app may only read. The token resolves to the same authority as an API key with the same team and read only choice.
- A connection is listed with the API keys in **Settings > Developer**, and revoking it there disconnects the app. API keys work as before.

**Changed: MCP tool results are structured.**

- Each MCP tool declares an `outputSchema`. The schema is the response schema of its endpoint, without `success`.
- Each tool result has `structuredContent`: the `data` of the response, and the `pagination` of a list.
- The text of a result starts with one line that names what came back, by name and id. The JSON of the result follows it.
- The MCP server adds `brokerplus_search_operations`, `brokerplus_describe_operation`, and `brokerplus_call_operation`. They find, describe, and call each API operation that the server publishes. Refer to [MCP server](/mcp#find-and-call-any-operation).
- The MCP server sends `401` when the API refuses the key. Before, the first tool call failed instead.
- The MCP server does not show write tools to a read-only key.

**Changed: booking, moving, and canceling a meeting, and asking for intake changes, now need the broker's approval.** This is a breaking change.

- `POST /v1/appointments/book`, `POST /v1/appointments/{id}/reschedule`, `POST /v1/appointments/{id}/cancel`, and `POST /v1/intakes/{id}/request-changes` now return `202` and a pending approval. Before, they returned `201` or `200` and emailed the client immediately. Nothing changes and nobody is emailed until the broker approves the request in BrokerPlus. Refer to [Messaging and approvals](/approvals).
- A booking approval expires at `start_at`, or after seven days if that is sooner. If the time is taken before the broker approves, the approval is `failed` and nothing is sent.
- A second cancel of a canceled booking now returns `409 BOOKING_CANCELED`.
- `GET /v1/appointments/{id}/booking` reads the booking on an appointment, including after a move or a cancellation.

**Changed: the `ltv` on an equity report is a percent.**

- `ltv` on `GET /v1/equity-reports` was a decimal fraction, such as `0.488`. It is now a percent, such as `48.8`, the same unit as every other rate and ratio in the API. If you divide it by 100 or show it as a fraction, change that code.

**Added: how an equity report reached the client.**

- Each equity report has `channel`: `email` or `sms`.

**Added: the record type, and names beside ids.**

- Every record now has an `object` field that names its type, for example `"deal"`, `"contact"`, or `"task"`. It is always present and never changes for a type. Contact methods, relationships, approvals, and automation runs have it too.
- A deal adds `board_name`, `assigned_broker_name`, `underwriter_name`, and `lender_name`.
- A task adds `deal_name`, `contact_name`, and `assignee_name`.
- A contact adds `owner_name`. A note on a deal or a contact adds `author_name`.
- An appointment adds `owner_name` and `deal_name`. A draft adds `sender_name`.
- A submission adds `submitted_by_name`, and `lender_name` now has a value on a BrokerPlus submission.
- No field changed its name or its type. Each name is for display: keep the id as your reference.
**Added: more of what a broker sees on contacts, tasks, conditions, and their lists.**

- A contact has `nickname`, `date_of_birth`, `phone_extension`, `language`, `assigned_broker_id`, `not_a_borrower`, `is_entity`, `opt_out_milestones`, `created_via`, and `archived_at`. `GET /contacts/{id}` returns an archived contact; `GET /contacts` still leaves it out. `POST /contacts` and `PATCH /contacts/{id}` take `nickname`, `date_of_birth`, and `phone_extension`, and `PATCH` also takes `language`.
- A task has `parent_task_id`, `provenance`, `progress`, `snoozed_until`, and `completed_by_user_id`. `PATCH /tasks/{id}` takes `progress` and `snoozed_until`.
- A condition has `lender_condition_name`, `plain_title`, `plain_description`, `tags`, `needs_review`, `status_note`, `status_note_at`, `resolved_at`, `satisfied_by_user_id`, `auto_satisfied_document_id`, and `source_communication_id`. `PATCH /conditions/{id}` takes `status_note`.
- A document has `excluded_from_submission`.
- A lead has `lead_form_id` and `lead_form_name`.
- A note on a deal or a contact has `type`, the kind of note the brokerage filed it as.
- A tag has `is_type` and `updated_at`.
- A consent has `text`, `text_version`, `locale`, `created_at`, and `updated_at`.
- `GET /referral-sources` returns `type`, `company`, `contact_id`, `contact_details`, `notes`, `last_contact_at`, and `next_follow_up_at` for each partner.
- `GET /custom-fields` returns `id`, `default_value`, `position`, `created_at`, and `updated_at`.
- `GET /task-categories` returns `id`, `position`, `created_at`, and `updated_at` on a custom category.
- A referral source, a custom field, and a task category have `object`: `"referral_source"`, `"custom_field_definition"`, and `"task_category"`.

**Added: the rest of the mortgage application.**

- `GET /v1/deals/{id}/application` returns `label`, `loan_type`, `charge_type`, `repayment_type`, `requested_payment`, `down_payment_override`, `legal_and_closing_costs`, `type_of_buyer`, `loan_purpose_additional`, `mortgage_classification`, `product_code`, `rate_compound_frequency`, `lender_classification`, `early_payout_type`, `capitalize_payout_penalty`, `purchase_stage`, `purchase_timeline`, `improvement_description`, and `security_deposit_amount`. `PATCH` changes each of them except `product_code`.
- `GET /v1/deals/{id}/property` returns the heating, water, parking, zoning, construction and description types, `tenure`, the property and lot size with their units, `environmental_hazard`, `new_property`, `mls_listed`, the rental and condo fee rules, `annual_tax_estimated`, `tax_year`, `taxes_paid_by`, the original purchase price and date, seven monthly operating costs, `legal_description`, and `legal_pin`. `PATCH` changes each of them except `tenure`.
- An applicant returns `honorific`, `suffix`, `sex`, and `correspondence_language`. A job returns `industry_varies`, `business_type`, the employer's address and phone number, and `amount_basis` on each income line.
- An asset returns `applied_to_down_payment`. A liability returns `debt_type`, `description`, `original_loan_amount`, `closing_date`, `charge_type`, `repayment_type`, `redemption_status`, `rate_type`, `payoff_type`, `source_deal_id`, and `property_id`. An other property returns `street_type`, `street_direction`, `value_estimate`, `value_source`, the condo fee rules, `mortgage_rate_type`, `mortgage_payment_frequency`, `rental_calc_type`, and its monthly operating costs.
- The row endpoints accept the new fields that a broker can edit. Refer to [Applicants and financials](/model#applicants-and-financials).

**Changed: webhooks for brokerages with more than one team.**

- A brokerage with more than one team can now register and enable webhooks. The API does not
  return `409 MULTI_TEAM_UNAVAILABLE` for webhooks.
- An endpoint receives an event only when the key that registered it can read the record of that
  event now, with the same rules as `GET /v1/changes`. This is also true for a brokerage with one
  team. For example, an `email.received` event from a mailbox that the broker cannot see does not
  go to the endpoint. Refer to [Who receives an event](/webhooks#who-receives-an-event).
- When the broker is deactivated or leaves the team of the key, the endpoint stops receiving events.
- An endpoint belongs to the broker whose key registered it. A key of a different broker gets `404`
  for it, and `GET /v1/webhooks` does not show it.
- The limits on a key apply to webhooks. A key with `no_documents` gets `403 KEY_LIMITED` when it
  subscribes to a document event, and its endpoint never receives one. Refer to
  [Who receives an event](/webhooks#who-receives-an-event).

**Added: limits on a key.**

- A key can now have the limits `leads_only`, `no_sending`, and `no_documents`, in addition to `read_only`. Set them when you create the key in **Settings > API keys**. Each limit removes access, and no limit can give a key more than its broker has. Refer to [Limits on a key](/authentication#limits-on-a-key).
- A request that a limit refuses returns `403 KEY_LIMITED`, with the limit in `details.limit`.
- `GET /v1/me` reports the limits of the key in `api_key.limits`.

**Changed: scheduling a campaign now needs the broker's approval.** This is a breaking change.

- `POST /v1/campaigns/{id}/schedule` now returns `202` and a pending approval. Before, it returned
  `200` and scheduled the campaign immediately. The campaign stays a draft until the broker approves
  the request in BrokerPlus. The response still carries `warnings`.
- The approval is bound to the campaign's words and its audience. A change to either one voids it.
  It expires at `scheduled_at`.

**Added: messages and approvals.** See [Messaging and approvals](/approvals).

- `POST /v1/contacts/{id}/messages` asks the broker to send one email or SMS to a contact.
- `POST /v1/drafts/{id}/send` asks the broker to send a draft that waits for review.
- `GET /v1/approvals` and `GET /v1/approvals/{id}` read each request and the broker's decision.
- `GET /v1/automation-runs`, `GET /v1/automation-runs/{id}`, and
  `POST /v1/automation-runs/{id}/start` read automation runs and ask to start one now.
  `POST /v1/automation-runs/{id}/cancel` stops a run that has not started.
- New webhook events: `approval.approved` and `approval.rejected`.
- `PATCH /v1/tasks/{id}` and `POST /v1/tasks/{id}/complete` return `409 APPROVAL_TASK` for an
  approval task.
- The MCP server exposes all of these except approval. Only a broker can approve.

**Added: who a deal's mail goes out as.**

- `GET /v1/deals/{id}/sender` returns the broker whose name the client sees, the mailbox the mail leaves from, the team brand, and the business name in the footer. BrokerPlus finds it the same way a send finds it, from the deal's assigned broker. Refer to [Automations](/automations#see-who-a-deals-mail-comes-from).
- The API cannot set the sender. To change it, assign another broker to the deal.
- The MCP server exposes it.

**Added: automations.**

- `GET /v1/automations` lists the Pulse automations on the broker's board, with their steps in order. Filter with `type`, `enabled`, and `updated_since`. `GET /v1/automations/{id}` reads one.
- Each step has `ask_before_sending`. When it is `true`, the step puts a draft in the broker's queue instead of sending.
- `POST /v1/automations` creates an automation that sends templates when a deal moves into a stage. `PATCH /v1/automations/{id}` changes one.
- The API saves an automation OFF, and a change turns it OFF again. The broker turns it on in BrokerPlus. Refer to [Automations](/automations).
- The MCP server exposes all four.

**Added: scheduling.**

- `GET /v1/meeting-types` lists the broker's meeting types, with the booking page URL, the form
  fields, and the questions of each.
- `GET /v1/availability` returns the open times of a meeting type. BrokerPlus calculates them the
  same way as the booking page, with each host's own time zone and split shifts.
- `POST /v1/appointments/book` books a contact into an open time. **It sends the client the booking
  confirmation email**, the same one the booking page sends. A time that is not open returns
  `409 SLOT_TAKEN`.
- `POST /v1/appointments/{id}/reschedule` and `POST /v1/appointments/{id}/cancel` move or cancel a
  booked appointment. Each one sends the client an email about the change.
- The MCP server adds `brokerplus_list_meeting_types`, `brokerplus_get_availability`, and
  `brokerplus_book_meeting`. It does not move or cancel a booking. Every read tool is now marked
  read-only.

**Added: condition writes.**

- `POST /v1/deals/{id}/conditions` adds a condition to a deal.
- `POST /v1/conditions/{id}/satisfy` and `POST /v1/conditions/{id}/waive` change its status.
- A condition now returns `assignee_role`, `assignee_contact_id`, and `assignee_contact_name`: who
  owes it. You can set the first two on a new condition and through `PATCH /v1/conditions/{id}`.
- A status change through the API now completes or reopens the task that decides the condition, as
  it does in BrokerPlus.

**Added: task cancel, reopen, and notes.**

- `POST /v1/tasks/{id}/cancel` and `POST /v1/tasks/{id}/reopen`.
- `GET /v1/tasks/{id}/notes` and `POST /v1/tasks/{id}/notes`.

**Added: ten webhook events.** `contact.created`, `deal.updated`, `deal.status_changed`, `deal.archived`, `deal.restored`, `condition.created`, `condition.satisfied`, `task.updated`, `sms.received`, and `intake.document_uploaded`. A `deal.updated` delivery carries `changed_fields`, which names the deal fields that changed. See [Webhooks](/webhooks).

**Added: the webhook delivery log.** `GET /v1/webhooks/{id}/deliveries` lists what BrokerPlus sent to an endpoint in the last 30 days, with the status, the number of attempts, and the last error of each delivery. `POST /v1/webhooks/{id}/deliveries/{delivery_id}/retry` sends a `failed` delivery again.

**Changed: an endpoint is disabled after 20 failed events, not 20 failed attempts.** Before this change, each of the 6 attempts at one event counted as a failure, so 4 bad events could disable an endpoint.

**Fixed: webhook deliveries are no longer lost after an outage.** Before this change, BrokerPlus looked back only 5 minutes for events to send. Events older than that were never sent. Now BrokerPlus sends every event that it has not sent, for up to 72 hours. A busy brokerage also no longer loses events: internal activity can no longer fill a send cycle before your events.

**Changed: a re-enabled endpoint starts from the time you enable it.** BrokerPlus does not send the events from the time that the endpoint was disabled.

**Added: email and text templates.**

- `GET /v1/templates`, `GET /v1/templates/{id}`, `POST /v1/templates`, `PATCH /v1/templates/{id}` and
  `DELETE /v1/templates/{id}` read and change the email and text templates in the Pulse library. A
  delete archives the template. See [Templates](/templates).
- `GET /v1/templates/merge-fields` lists every merge field that a template can use, including your
  brokerage's custom fields. A template that uses a merge field that is not in the list is refused
  with `400 UNKNOWN_MERGE_FIELDS`.
- A template sends nothing by itself. A Pulse automation sends it.
- The MCP server exposes every template operation except the archive.

**Changed (breaking): every rate on an opportunity is a percent.** `target_new_rate` and
`breakeven_rate` were decimal fractions. They are now percents, the same as `interest_rate`. For
example, `0.0499` is now `4.99`. If your integration multiplies these two fields by 100, remove
that step.

**Added: actions on an opportunity.**

- `POST /v1/opportunities/{id}/convert` starts a new working deal from a tracked mortgage. The
  deal gets the borrowers and the mortgage facts, and the opportunity stays on the book. The
  response is the new deal. Each call creates a deal, so send `Idempotency-Key` to make a retry
  safe.
- `POST /v1/opportunities/{id}/dismiss` takes a mortgage off the book and stops its outreach.
  `POST /v1/opportunities/{id}/restore` puts it back.
- `POST /v1/opportunities/{id}/snooze` hides an opportunity until the date in `until`, and
  reminds the broker that morning. `POST /v1/opportunities/{id}/unsnooze` ends the snooze.
- An opportunity now returns `dismissed_at`.

**Added: the client on an equity report.** An equity report now returns `contact_id` and
`contact_name`. `GET /v1/equity-reports` takes `contact_id`, to list the reports about one client.

**Added: the change feed.**

- `GET /v1/changes` lists every change to contacts, leads, deals, tasks, intakes, documents,
  appointments, and communications, in order, with a checkpoint. BrokerPlus writes each entry in the
  same database transaction as the change, so the feed has every change, from every source.
- Each entry gives `record_type`, `record_id`, `action` (`created`, `updated`, `deleted`, `merged`,
  or `archived`), the names of the changed fields, and, for a merge, `merged_into_id`.
- Send `after=now` to get a checkpoint before a full sync. Filter with `types`.
- BrokerPlus keeps entries for 30 days. An older checkpoint gets `410 CHECKPOINT_EXPIRED`.
- Webhooks are hints. Use the feed as the source of truth. See
  [Webhooks are hints](/webhooks#webhooks-are-hints).
- The MCP server exposes it as the read-only tool `brokerplus_list_changes`.

**Added: intake progress for each applicant.** An intake now contains `applicants`. Each entry gives
the applicant's `name`, `role`, `submitted_at`, the required questions `answered` out of `total`, and
the `unanswered` questions for each section. The progress moves as the client types, before they
submit. See [Intakes](/intake).

**Added: read an intake's answers.** `GET /v1/intakes/{id}/answers` returns every question each
applicant is asked, grouped by section, with the answer as it stands now. The SIN is never returned.

**Added: approve an intake, or ask the client for changes.**

- `POST /v1/intakes/{id}/approve` is the same action as **Approve** in BrokerPlus. The client gets no
  message.
- `POST /v1/intakes/{id}/request-changes` reopens the answers you name and **emails the client**, the
  same as a change request in BrokerPlus.

**Added: read the drafts you filed.** `GET /v1/drafts` and `GET /v1/drafts/{id}` return the drafts
that `POST /v1/contacts/{id}/drafts` filed, with what the broker did with each one. A draft now
returns `recipients`, `sender_user_id`, `created_by_user_id`, `scheduled_at`, `reviewed_at`,
`communication_id`, and `updated_at`.

**Changed: a draft's `status` can now be `scheduled`, `sent`, `discarded`, or `failed`.** A new draft
is still `pending_review`. Before this change, `status` was always `pending_review`.

**Changed: `POST /v1/tasks/{id}/complete` documents its body.** The optional `resolution` was always
accepted. It is now in the reference, and the MCP tool can send it.

**Changed: the MCP server.**

- An error now includes the API's `details`, such as the matching contacts on `DUPLICATE_CONTACT` or
  the fields that failed on `VALIDATION_ERROR`.
- Every tool now has MCP annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and
  `openWorldHint`.
- New tools: `brokerplus_list_task_categories`, `brokerplus_check_deal_stage_requirements`,
  `brokerplus_list_deal_team`, `brokerplus_get_intake_link`, `brokerplus_list_intakes`,
  `brokerplus_get_intake`, `brokerplus_get_intake_answers`, `brokerplus_list_drafts`, and
  `brokerplus_get_draft`. The MCP server does not approve an intake or ask a client for changes.

**Added: edit the mortgage application one row at a time.** See
[Edit the application one row at a time](/model#edit-the-application-one-row-at-a-time).

- Applicants, jobs with their pay, other income, assets, liabilities, and other properties each have
  a `POST`, a `PATCH`, and a `DELETE`. A write changes one row, and BrokerPlus then calculates the
  totals and the ratios again.
- A liability from a credit report stays read-only, and a write to it returns
  `409 FROM_CREDIT_BUREAU`.
- `GET /v1/deals/{id}/applicants/{applicantId}` reads one applicant, and
  `GET /v1/deals/{id}/other-properties` lists the other properties.

**Added: the calculated figures on the application.**

- `GET /v1/deals/{id}/application` now returns `gds`, `tds`, `ltv`, and `ratio_qualifying_rate`, as
  percents. It also returns `total_loan_amount`, `insurance_premium`, `insurance_premium_rate`,
  `insurance_premium_added_to_loan`, `rate_capped`, `loan_purpose_renewal`,
  `loan_purpose_improvements`, `improvement_cost`, `payment_accelerated`, `insurer_policy_number`,
  and `stated_fees`, the fees that the lender stated.
- The property now returns `appraised_value` and `appraisal_date`.
- A job now returns `incomes`, its pay lines. An asset and a liability now return
  `owner_contact_ids`.
- `GET /v1/deals/{id}/application/terms-history` lists each change to the loan terms, with who made
  it and when.

**Fixed: a sync with `updated_since` can no longer skip a row.**

- Every list that takes `updated_since` now also takes `after_id`. Send the `updated_at` and `id` of
  the last row you read, and keep `page` at 1. The next page starts after that row. Before this, a
  row that changed while you paged moved to the end of the list and pushed the next row back onto a
  page you had already read, so you never saw it.
- With `updated_since`, every list is in `updated_at` order and then `id` order, oldest first. Rows
  with the same `updated_at` now come back in the same order on every request.
- `GET /v1/opportunities` with `updated_since` is now oldest first, as documented. It was newest
  first.
- `GET /v1/campaigns` now filters on `updated_since`. It ignored it before.
- `GET /v1/communications` with `updated_since` now selects on the new `updated_at` field. It moves
  when a message is read, starred, filed on a deal, or moved to a different folder. Before this the
  filter used `created_at`, so a sync never saw those changes. Every message gets an `updated_at`
  at the time of this release, so your next sync reads all your mail one time.

**Added: fields you write can be read back.**

- A task returns `priority`, `pinned` and `resolution`.
- A lead returns `first_name`, `last_name`, `email` and `phone`, from the lead's contact.
- A campaign returns `body_markdown`: the email body as Markdown that gives exactly the HTML in
  `body`. It is null for a campaign written in BrokerPlus and for an SMS campaign.

**Fixed: a campaign written in BrokerPlus returns the HTML that is mailed in `body`.** It returned a
plain-text copy before this.

**Fixed: a refused `PATCH /v1/deals/{id}` saves nothing.** Before this, the `underwriter_id` and
`lender_id` in a request were saved even when the API refused the stage, status or another field in
the same request. Now the whole request is saved, or none of it is. A stage that needs a lender or an
underwriter is still entered by the request that sets it.

**Fixed: a key can no longer stay rate-limited after its minute ends.** A rare failure could leave
a key's request count with no end time, and every later request got `429 RATE_LIMITED`.

**Changed: a query parameter that the API cannot read gets `400 VALIDATION_ERROR`.**

- An `updated_since` that is not an ISO 8601 date-time gets `400 VALIDATION_ERROR`. Before this,
  the API ignored it and returned the full list.
- A filter value that is not allowed, for example `GET /v1/tasks?status=x` or
  `GET /v1/communications?channel=x`, gets `400 VALIDATION_ERROR` with the allowed values in
  `message`. Some of these returned `500` before this.
- An invalid `deal_id`, `contact_id`, `source`, `status`, `starts_after` or `starts_before` filter
  now gets `400 VALIDATION_ERROR`. Some lists returned `400 BAD_REQUEST`. `BAD_REQUEST` is now only
  for an ID in the path.
- `details.fieldErrors` names the parameter.
- `GET /v1/intakes?contact_id=...` no longer returns intakes from teams the broker cannot see.

**Added: referral partners.**

- `GET /v1/referral-partners` reads the partner book, as the Partners page shows it. For each
  partner, you see the people they referred, the deals that funded and their value, and the people
  the broker introduced to them. The figures cover the whole history. Send `period` (`ytd`, `qtd`,
  `mtd`), or `start` and `end`, to read a shorter period.
- `GET /v1/referral-partners/{id}` reads one partner, with each person, funded deal, and
  introduction listed.
- A deal now returns `referral_partner`. `POST /v1/deals/{id}/referral-partner` sets it from a
  contact or from a name. `DELETE /v1/deals/{id}/referral-partner` clears it.
- The MCP server exposes the two reads and the set.

**Added: reports.** `GET /v1/reports/pipeline` gives the open deals and their loan amounts on each
stage. `GET /v1/reports/funded` gives the deals that funded and their volume, in total and for each
month of a period. Both give the same figures as the Business pages. The MCP server exposes both.

**Added: merge, duplicates, archive, and more ways to reach a contact.**

- `POST /v1/contacts/{id}/merge` merges the contacts in `duplicate_ids` into this one, the same as
  Merge on the Duplicates page. It returns the contact that survives. A merged id now returns
  `410 CONTACT_MERGED` on `GET /v1/contacts/{id}`, with the survivor's id in
  `details.merged_into_id`.
- `GET /v1/contacts/duplicates` lists the sets of contacts that look like the same person.
- `POST /v1/contacts/{id}/archive` and `POST /v1/contacts/{id}/unarchive` take a contact out of
  the book and put it back. A contact now returns `archived_at`.
- `GET`, `POST` and `DELETE` on `/v1/contacts/{id}/methods` read, add and remove the extra emails,
  phones and social profiles on a contact. A social profile returns its `url`.
- `GET /v1/contacts/{id}/relationships` lists the people a contact is related to.
- The MCP server exposes the merge and the three reads. The merge tool is marked destructive, so
  an assistant asks before it merges.

**Added: a broker's licences.** A user now returns `licenses`: one entry for each province, with the
licence class and the day it expires.

**Docs: corrections and pages for AI agents.** The API did not change.

- [Pagination](/pagination) now gives the real order of each list. Not every list is newest first.
- The [Introduction](/introduction) now says that you can update the loan terms and the property
  address on a deal.
- [Errors](/errors) now lists `409 MULTI_TEAM_UNAVAILABLE` and `503 TRY_AGAIN`.
- Every page is also available as Markdown at its URL plus `.md`. `/llms.txt` indexes the pages,
  and `/llms-full.txt` contains all of them.
- The examples show curl, TypeScript, and Python.
- New guides: [Capture leads from your website or ads](/capture-leads),
  [Sync your book](/sync-your-book), [Migrate from another CRM](/migrate-from-a-crm), and
  [Build a Claude command center](/mcp).

**Added: document uploads.**

- `POST /v1/deals/{id}/documents` adds a file to a deal. Send it as `multipart/form-data` (50 MB maximum), or as JSON with `content_base64` (10 MB maximum). You can also send `document_type`, `contact_id`, and `name`. BrokerPlus stores, classifies, and checks the file the same way it does for a file that a broker uploads. See [Files](/files).
- A new `document.uploaded` webhook fires when a file is added to a deal by a broker, by the API, or from an email.
- `GET /v1/deals/{id}/document-requests` lists the documents that a client still owes on a deal.
- `GET /v1/document-types` lists the codes for `document_type`.
- The MCP server exposes the upload, in base64, and both lists.

**Added: more fields and filters on documents.**

- A document now returns `contact_name`, `display_name`, `document_type_label`, `stage`, `verified`, `folder`, `uploaded_by_user_id`, and `uploaded_by_name`.
- `uploaded_via` now returns a fixed set of values, and can be `api` for a file that this API uploaded.
- `GET /v1/deals/{id}/documents` takes `verified`, `document_type`, `contact_id`, and `updated_since`. An `updated_since` that is not a date returns `400`.

**Added: find a contact or a lead by email or phone.**

- `GET /v1/contacts` and `GET /v1/leads` take `email`, `phone`, and `q`. The `email` match ignores case. The `phone` match compares the last 10 digits, so any format works. Both match the main value and any additional email or phone on the contact. `q` is the same text search as the search bar in BrokerPlus.
- On `GET /v1/leads`, each filter matches the lead's contact. No match returns an empty list, not `404`.

**Added: update, archive, and convert a lead.**

- `PATCH /v1/leads/{id}` changes the stage, service type, first-time buyer flag, follow-up date, details, referral source, and channel. The name, email, and phone stay on the contact.
- `POST /v1/leads/{id}/archive` and `POST /v1/leads/{id}/unarchive` take a lead out of the funnel and put it back.
- `POST /v1/leads/{id}/convert` starts a deal from the lead. You can name the deal `type`, `board_id`, and `stage_id`. A second call returns the same deal.
- A lead now returns `status` (`open`, `archived`, or `converted`), `converted_deal_id`, `referral_source_id`, `channel_id`, and the name next to each id.
- `GET /v1/lead-stages`, `GET /v1/referral-sources`, and `GET /v1/lead-channels` list the ids that a lead takes.

**Added: custom fields on leads, and filters by custom field.**

- `POST /v1/leads` and `PATCH /v1/leads/{id}` take `custom_fields`, a map of field `key` to value. A lead's custom fields are the fields of its contact. One bad key or value refuses the whole request, and `details.custom_fields` names each one.
- `GET /v1/leads/{id}/custom-fields` and `PATCH /v1/leads/{id}/custom-fields/{key}` read and set them one at a time.
- `GET /v1/custom-fields` lists the field definitions. Send `entity=contact` or `entity=deal` to list one kind.
- `GET /v1/contacts`, `GET /v1/leads`, and `GET /v1/deals` take `custom_field[<key>]=<value>`.
- The MCP server exposes the lookup, the lead update, archive and unarchive, the conversion, and the lead custom fields.

## 2026-10-06

**Added: deal custom fields.**

- `GET /v1/deals/{id}/custom-fields` reads the custom fields your brokerage keeps on a deal. `PATCH /v1/deals/{id}/custom-fields/{key}` sets one. They work the same as the contact custom fields: address a field by its `key`, and send an empty string to clear a field.
- The MCP server exposes both.

**Added: the `short_text` custom field type.** A custom field can now have the `type` `short_text`. It holds one line of 200 characters or fewer. When you set one, line breaks in the value change to spaces. A longer value returns an error.

## 2026-10-05

**Added: you can correct what a tracked mortgage carries.**

- `PATCH /v1/opportunities/{id}` takes `balance`, `interest_rate`, `prime_adjustment`, `monthly_payment`, `property_value`, `effective_penalty`, and `penalty_type`. Send only the fields you are changing. `interest_rate` and `prime_adjustment` are percents, although the other rates on an opportunity are decimal fractions.
- A balance you send becomes the new anchor. BrokerPlus pays it down from that day onward, and neither the nightly recalculation nor an import overwrites it. Send null to hand the balance back to BrokerPlus.
- A rate you send also lands on the mortgage application, so the deal and the book agree.
- An opportunity now returns `balance_is_manual`, `interest_rate`, `prime_adjustment`, and `penalty_type`.
- `GET /v1/opportunities` takes `deal_id`. A deal does not carry the id of its opportunity, so this is how you go from a deal to the mortgage it tracks.
- `effective_penalty` now returns the penalty a broker entered from a lender payout statement, when there is one. Before this it returned only the penalty BrokerPlus calculates.

## 2026-10-03

**Added: `express_send` on campaigns.**

- `POST /v1/campaigns/{id}/schedule` takes an optional `express_send`. When it is `true`, the whole audience gets the campaign within about two hours of `scheduled_at`, and `send_window_days` is ignored. A fast send from one mailbox is more likely to go to spam, so use it only when speed matters more than delivery.
- A campaign now returns `express_send`.

**Changed: paced sends wait for daytime.** Without `express_send`, a campaign only sends between 8 AM and 8 PM. A recipient whose address places them in a Canadian province gets it on their own clock; everyone else goes by the time zone of the person who created the campaign. A `scheduled_at` outside those hours starts at the next 8 AM, and each day of a multi-day send stays inside those hours.

**Added: `skip_weekends` on campaigns.** `POST /v1/campaigns/{id}/schedule` takes an optional `skip_weekends`, which holds a paced send over Saturday and Sunday and carries on Monday at 8 AM. A campaign now returns `skip_weekends`.

**Added: `warnings` on a schedule.** `POST /v1/campaigns/{id}/schedule` now returns `warnings`, a list of plain sentences to pass on to the broker: one day of the send close to the daily sending limit of their connected mailbox (Google Workspace 2,000, Gmail 500, Microsoft 365 10,000, Outlook.com 1,000), or an Express send starting outside 8 AM to 8 PM. The campaign is scheduled either way.

**Changed: a busy database no longer fails a schedule with a bare `500`.** `POST /v1/campaigns/{id}/schedule` now answers `503 TRY_AGAIN` when BrokerPlus is too busy to finish. The campaign stays a draft and nothing is sent, so send the same request again after a short wait.

## 2026-10-01

**Added: the client intake works over the API.** See [Intakes](/intake).

- `GET /v1/intake-link` returns the broker's standing intake link, the one that never expires. `PATCH /v1/intake-link` turns it on or off, and the url stays the same through a disable.
- `GET /v1/intakes` lists the intakes a client is filling in, with filters for `status`, `deal_id`, `contact_id`, and `updated_since`. `GET /v1/intakes/{id}` reads one.
- `POST /v1/intakes` asks one client for an intake. A deal is optional, a second applicant can be named, and `delivery` of `email` has BrokerPlus send the link from the broker's own mailbox.
- `POST /v1/intakes/{id}/link` issues a fresh link for a client. BrokerPlus keeps only a fingerprint of a link it has sent, so a link cannot be read back. Every link issued for the same intake works until it expires.
- Two new webhooks: `intake.started` when a client first opens an intake, and `intake.submitted` when they send it back.

**Added: a connected mailbox is readable over the API.**

- A communication now returns `from`, `to`, and `cc`, so you can see who an email was between.
- A communication now returns `body_html`, which is the HTML the sender wrote. It arrives exactly as it was sent and it is never sanitized, so sanitize it before you render it.
- A communication now returns `attachments`, which names each attached file, its type, and its size.
- `GET /v1/communications/{id}/attachments/{attachment_id}` returns a signed link to one attached file, valid for five minutes. BrokerPlus gets the file from the mail provider the first time somebody asks for it. If the mailbox no longer holds the message, the response is `410 ATTACHMENT_UNAVAILABLE`.
- A communication now returns `read_at`, which is null for mail nobody has opened, and `starred`.
- A communication now returns `folders`, which is every folder and label the message sits in, as your mail provider names them, and `mailbox`, which is the connected address it arrived at.
- `GET /v1/communications` now filters on `thread_id` for one conversation, on `unread` for mail nobody has opened, and on `folder` by role (`inbox`, `sent`) or by the provider's own folder name.
- A new `email.received` webhook fires when mail arrives in a connected mailbox. Junk mail and the history BrokerPlus imports when a mailbox is first connected do not fire it.

A key reads the mailboxes its own broker can read in BrokerPlus. A teammate who has not shared their
mailbox is no more readable through the API than through the app.

## 2026-09-28

**Added: you can name a deal, record its lender file number, set the date it funded, and archive it.**

- `PATCH /v1/deals/{id}` now takes `name`, which is the name a broker gives a deal. Send null or an empty string to put the deal back on your brokerage's naming pattern. A deal now returns `name`.
- `PATCH /v1/deals/{id}` now takes `lender_file_ref`, which is the lender's own file number for the deal. Use it for a number that comes from somewhere other than BrokerPlus. `reference_number` stays ours, and stays read-only.
- `PATCH /v1/deals/{id}` now takes `funded_at`, so a deal that funded before it reached BrokerPlus carries its real funding date. It is only accepted on a deal that is funded, or in the same request that sets `status` to `funded`. On any other deal it returns `409 NOT_FUNDED`.
- `DELETE /v1/deals/{id}` archives a deal. The deal leaves every board and report, and a broker can restore it in BrokerPlus. It is the same archive the dashboard runs, so the deal's open tasks and queued messages stop with it.

**Added: the mortgage application takes the purchase price, the payment, the insurer, and two more dates.**

- `PATCH /v1/deals/{id}/application` now takes `purchase_price`, `payment_amount`, `payment_frequency`, `mortgage_insurer`, `interest_adjustment_date`, and `funding_date`.
- The application now returns `interest_adjustment_date` and `mortgage_insurer`.
- `PATCH /v1/deals/{id}/property` now takes `occupancy_type` and `occupancy_purpose`.

**Fixed: a stage requirement with a fixed list of choices now says what it takes.**

- Filling a requirement such as the property's province or the payment frequency with a value that is not on its list returned `500`. It now returns `400`, and the message names every value the field accepts.

## 2026-09-27

**Added: you can write the team roles, the mortgage terms, and the property address of a deal.**

- `GET /v1/deals/{id}/team` lists the users in your brokerage who hold a role on the deal, e.g. `Sales Manager`. `POST /v1/deals/{id}/team` fills a role, and `DELETE /v1/deals/{id}/team/{role}` empties it.
- A team role is one of your own users. A participant is a contact from outside your brokerage, e.g. a lawyer. They are different lists.
- `PATCH /v1/deals/{id}/application` changes the loan amount, rate, rate type, term, amortization, first payment date, and financing condition date. Rates are percents.
- `PATCH /v1/deals/{id}/property` changes the property address.
- `PATCH /v1/deals/{id}` now also takes `underwriter_id` and `lender_id`. A deal now returns both fields.
- The mortgage application now returns `financing_condition_date`.

**Added: `GET /v1/deals/{id}/stage-requirements` tells you what a deal needs before it moves to a stage.**

- Send the stage as `stage_id`. The response lists each missing requirement. It is the same check that a move runs, so an empty list means the move passes the stage requirements.
- Use it before a move, instead of reading the `missing` list from a `409` response.

**Added: a campaign can have follow-ups, and each one is a campaign with `follow_up_of` set.**

- A broker adds up to three follow-ups to a draft campaign in BrokerPlus. Each follow-up goes out a set number of days after the previous message, and only to people who did not reply.
- `GET /v1/campaigns` returns each follow-up as its own campaign. `follow_up_of` is the id of the campaign that it follows. `follow_up_delay_days` is the number of days that it waits. On an original campaign, both fields are null.
- `POST /v1/campaigns/{id}/schedule` refuses a follow-up with `NOT_A_ROOT`. A follow-up goes out with the campaign that it follows.
- The same endpoint refuses a `recurrence` on a campaign that has follow-ups, with `FOLLOW_UPS_WITH_RECURRENCE`. A campaign can repeat, or it can have follow-ups, but it cannot do both.
- The API cannot add or change follow-ups. The broker sets them in BrokerPlus.

## 2026-09-26

**Changed: a lead you send with `POST /v1/leads` now has the source `api`, and starts the same automations as a lead form.**

- Before this change, the lead had the source `manual`. A manual lead does not start the "New lead created" automation, and it does not send the `lead.created` webhook. A lead from the API now does both, the same as a lead from a BrokerPlus lead form.
- A lead that a broker types in BrokerPlus is still `manual`, and it still starts nothing.
- `source` on a lead can now be `api`. If your code reads `source`, accept the new value.

**Fixed: a deal with no type no longer fails `GET /v1/deals`.**

- `type` on a deal can now be `null`. A deal made from a lead has no type until the broker sets one.
- Before this change, a page of `GET /v1/deals` that held such a deal returned `500`, and so did `GET /v1/deals/{id}` for that deal.

**Fixed: a lead from a shared client link no longer fails `GET /v1/leads`.**

- `source` on a lead can be `portal`. The contract did not list it, so a page that held such a lead returned `500`.

## 2026-09-25

**Changed: a deal's `contacts` now carry `role_label`, which is what tells one realtor from the other.**

- Both agents on a purchase hold the same `realtor` role, so the seller's agent is told from the buyer's by the brokerage's own word for the seat - `Listing Realtor` and `Buying Realtor`.
- The participant endpoints already returned `role_label`. Reading a deal now returns it too, so naming one of the two agents takes one request instead of two.
- To record the distinction, attach the partner with `role: "other"` and your own `role_label`.

**Added: `POST /v1/campaigns/{id}/schedule` sets the time a campaign goes out.**

- The campaign must be a draft. Send `scheduled_at`, and optionally `send_window_days` to spread the send, or `recurrence` to repeat it.
- The recipient list is frozen at the moment you schedule. Until the campaign fires, the broker can still pause or delete it in BrokerPlus.
- **There is still no endpoint that sends a campaign immediately.** A schedule names a future time, which is what makes it safe to publish.
- The endpoint refuses a campaign whose audience resolves to nobody, and one whose brokerage has no CASL sender identity. The MCP server exposes it as `brokerplus_schedule_campaign`.

**Changed: `body` on `POST /v1/campaigns` is Markdown, and now becomes real formatting.**

- `**bold**`, `*italic*`, `[text](https://example.com)`, `#` headings, and `-` or `1.` lists render as formatting in the email. Before this change the asterisks reached the client as text.
- **Write Markdown, not HTML.** Tags in this field are shown to the client as text. This protects a broker's own mailbox from sending markup it did not write.
- An SMS campaign is unchanged: it sends the body as plain text, with no formatting.
- `body` on a campaign you read back is the HTML that is mailed, because that is what BrokerPlus stores.

## 2026-09-23

**Added: campaigns, audiences, contact custom fields, and equity reports.**

- `GET /v1/campaigns` and `GET /v1/campaigns/{id}` read your newsletters and mass messages. Each campaign carries a `results` object with the counts per outcome, which includes `opened` and `unsubscribed`. The counts come from the recipient rows, so they agree with what BrokerPlus shows you.
- `GET /v1/campaigns/{id}/recipients` reads the result for each person: the contact id, the status, `opened_at`, and `unsubscribed`. Filter it with `status` or with `opened`. **Clicks are not recorded.** BrokerPlus does not rewrite the links in your message, so no endpoint reports a click.
- `POST /v1/campaigns` creates a campaign as a **draft**. `POST /v1/audiences` saves the list of contacts to send it to, and `GET /v1/audiences` lists the ones you have. An audience is a fixed set of contacts, not a saved filter: a contact who matches your criteria tomorrow does not join a list that you save today.
- **The API never sends a campaign, and never schedules one.** The broker opens the draft in BrokerPlus, reads it, and sends it there. There is no endpoint that sends, and this release does not add one.
- `GET /v1/contacts/{id}/custom-fields` reads the custom fields your brokerage keeps on a contact. `PATCH /v1/contacts/{id}/custom-fields/{key}` sets one. Address a field by its `key`, which does not change when somebody renames the label. Send an empty string to clear a field.
- `GET /v1/equity-reports` and `GET /v1/equity-reports/{id}` read the monthly reports sent to past clients. The figures are the snapshot the client received, not today's numbers; read today's numbers from `GET /v1/opportunities`. `cta_clicked_at` is the client asking for a call.
- The MCP server exposes all of these, except the campaign send that does not exist.

## 2026-09-22

**Added: `POST /v1/contacts/{id}/drafts` files a message for the broker to review.**

- Send a `channel` of `sms` or `email`, and a `body`. The draft lands in the review queue of the broker your key acts as, with the status `pending_review`.
- **The API never sends a draft.** The broker reads it in BrokerPlus, and approves, edits, or rejects it there. There is no endpoint that sends, and this release does not add one.
- Use this endpoint when your own system, or an assistant that you run, writes the message and a person must read it before a client does. See [Drafts](/model#drafts).
- The MCP server exposes the endpoint as `brokerplus_create_contact_draft`.

## 2026-09-21

**Added: a key can be created with read-only access.**

- When you create a key in **Settings > Developer**, set its access to **Read only**. Such a key answers a `GET` request and refuses every other method with `403` and the code `READ_ONLY_KEY`. See [Authentication](/authentication).
- The access is set once, at creation, and you cannot change it afterward. Revoke the key and create another one.
- **Every key that existed before this date has full access, and its behaviour does not change.**
- `GET /v1/me` now reports `api_key.read_only`.

## 2026-09-13

**Changed: a deal's `status` is one of a fixed set of words.**

- `status` on a deal is now one of `active`, `submitted`, `approved`, `funded`, `closed`, `dead`, `declined`, `cancelled`, `expired`, `withdrawn`, or `unknown`. A deal that carried `new` now carries `active`. A word that the set does not contain now reads `unknown`. The field type is unchanged: it is a string.
- The `status` filter on `GET /deals` accepts the same words.

## 2026-09-10

**Added: an MCP server, contact statistics, deal lookup by external reference, deal commission, deal consents, communication preferences, deal participants, team members, contact addresses, Finmo lenders and submissions, stage history, deal and task creation, and task assignment.**

- `/deals/{id}/participants` lists, attaches, updates, and removes the contacts on a deal, with their role. Borrower and partner roles share one list, and `is_partner` separates them. A partner with role `other` carries your own `role_label`. The endpoint supports `Idempotency-Key` on create.
- `GET /teams/{id}/members` lists who is on a team, their role on it, and their title.

- The [MCP server](/mcp) lets an AI assistant call the API with your key. Each tool is one documented endpoint, with the same inputs. It publishes reads across the API and the supported creates and updates. It publishes no delete, no webhook administration, no consent or commission write, and no document URL.
- `GET /contacts/{id}/statistics` answers a contact's deal counts, funded volume with its basis, repeat-client standing, referrals, and last touch.
- A deal now carries `lender_file_ref` and `external_references`: its ids in Velocity, Filogix, or Finmo. `GET /deals` takes `external_reference`, `source`, and `lender_file_ref` to find a deal by one of them. Matches are exact.
- `/deals/{id}/commission` reads, records, changes, and removes what the brokerage earns on a deal, with its line items and its splits. The same compensation access rule as the dashboard applies. The endpoint supports `Idempotency-Key` on create.
- `/deals/{id}/consents` lists and records the consents on a deal. `POST /deals/{id}/consents/{consentId}/revoke` takes one back and keeps the row. Both POST endpoints support `Idempotency-Key`.
- `/contacts/{id}/communication-preferences` reads and changes a contact's email and SMS consent, by category. `opt_in` clears a withdrawal and never records express consent.

- `/contacts/{id}/addresses` lists, adds, updates, and removes a contact's addresses. Exactly one address is primary. Provinces use two-letter codes, and countries use `CA`, `US`, or `other`. The endpoint supports `Idempotency-Key` on create.

- `GET /lenders` and `GET /submissions` now include the lenders and submissions of the brokerage's connected Finmo account, marked `source: finmo`. The brokerage's own records are marked `source: brokerplus`. Both endpoints accept a `source` filter. A Finmo submission carries `lender_name`, `lender_status`, `funded_at`, `loan_amount`, `interest_rate`, and `term_months`. The `slug`, `attempt`, and `status` fields are now nullable.

- `GET /deals/{id}/stage-history` lists every stage move on a deal, newest first, with the stage it left, the stage it entered, who moved it, and when. The same record feeds the `deal.stage_changed` webhook. Imported deals carry history only from their import date onward.
- `POST /deals` opens a deal for the key's broker. It lands on the broker's default pipeline for its type, or on the pipeline you name, and takes that pipeline's team. Attach borrowers with `contacts`. The endpoint supports `Idempotency-Key`.

- `POST /tasks` files a manual task. The task takes the team of its deal or contact, or the broker's own team. Send `assignee_user_id` to assign it, or leave it blank to file it unassigned. The endpoint supports `Idempotency-Key`.
- `PATCH /tasks/{id}` accepts `assignee_user_id`. Reassignment applies the dashboard's eligibility rule. An ineligible assignee returns `400 INVALID_ASSIGNEE` and changes nothing. Send `null` to unassign.

## 2026-09-08

**Corrected: access checks and existing writes.** These corrections add no new endpoints.

- Deal and contact requests enforce the broker's team access, including related notes, documents, conditions, and submissions.
- Deal updates use the dashboard's stage checks, compliance rules, and owner handoff. Status writes accept `active`, `dead`, `closed`, or `funded`.
- A combined deal update commits its fields and events together. A later notification or closing action can still fail after that commit.
- Contact edits preserve calculated client types and enforce identity locks. Condition edits enforce the parent deal's lock.
- Deal closing and maturity dates accept `YYYY-MM-DD` or an ISO timestamp. Responses remain date-only strings.
- New notes inherit their parent's team, sanitize HTML, and record eligible mentions.
- A saved response requires the original API key, broker, and current access to its result. Access loss returns `404` without repeating the mutation.

Read [Idempotency](/idempotency) before retrying a write with an uncertain result. No new endpoints are included in these corrections.

## 2026-08-15

**Added: the surface grew from 40 endpoints to 60.** Read the sections below for what is new.

**Deals now expose the mortgage itself.**

- `GET /deals/{id}/application` returns the loan: amount, term, amortization, rate, and payment.
- `GET /deals/{id}/property` returns the property that secures the loan.
- `GET /deals/{id}/applicants` returns everyone on the application, with their employment history
  and their income.
- `GET /deals/{id}/financials` returns the assets, the liabilities, and the down payment sources.

Every lender-coded field on these endpoints arrives as a `code` and a readable `label`. A code that
we cannot name gives you a `label` of `Unknown`, so that "we hold a value that we cannot name" stays
different from "there is no value".

**Added: the opportunity book.** `GET /opportunities` and `GET /opportunities/{id}` return the
mortgages that your brokerage tracks, with the balance, the penalty to break, and the savings from a
move.

**Added: appointments.** `GET`, `POST`, `PATCH`, and `DELETE` on `/appointments`. An appointment
that BrokerPlus synced from Google Calendar or from Outlook returns `409` on a write. Change that
appointment in the calendar that owns it. A write here would look like a success, and the next sync
would silently undo it.

**Added: submissions.** `GET /submissions`, `GET /submissions/{id}`, and
`GET /submissions/{id}/decisions` return the deals that a broker sent to a lender, and what each
lender answered.

**Added: communications.** `GET /communications` and `GET /communications/{id}` return the email,
the calls, and the messages on a deal or a contact.

**Added: your brokerage and your teams.** `GET /organization`, `GET /teams`, and `GET /teams/{id}`.

**Added: `PATCH /conditions/{id}`.** You can now mark a condition satisfied, waived, or outstanding.
You cannot write `lender_status`. The lender owns that value, BrokerPlus refreshes it on each
decision poll, and a value that you wrote here would tell a broker that the lender accepted
something that the lender never saw.

**Breaking: a calendar date is now a `YYYY-MM-DD` string.** The affected fields are `closing_at`,
`maturity_date`, `first_payment_date`, `funding_date`, and `rate_hold_expires_at`.

```diff
- "maturity_date": "2029-03-15T00:00:00.000Z"
+ "maturity_date": "2029-03-15"
```

These columns hold a date. They hold no time and no time zone. As a timestamp at midnight UTC, every
client west of UTC rendered the day before. On a maturity date, that is a wrong answer, not a
formatting preference. A write still accepts either form.

**Changed: the documentation is now written in Simplified Technical English.** No behaviour changed.
The pages follow ASD-STE100, constrained by the Google developer documentation style guide, so that
they read the same way for a non-native reader and for a translation tool.

**Still out, on purpose.** You cannot create a deal or a task, reassign a task, or upload a file.
The API does not return an applicant's social insurance number, a liability's account number, a
credit report, a lender rate sheet, or compensation. Each page explains the reason where the subject
comes up.

## 2026-08-08

**Added: the surface grew from 16 endpoints to 40.** Nothing that was already published changed
shape, so no existing integration needed a change.

- **Users** (`GET /users`, `GET /users/{id}`) and **Pipelines** (`GET /pipelines`,
  `GET /pipelines/{id}`). These close a gap. The fields `owner_id`, `assigned_broker_id`,
  `assignee_user_id`, `board_id`, and `stage_id` were published as ids with nothing to resolve them
  against. They now resolve.
- **Documents** (`GET /deals/{id}/documents`, `GET /documents/{id}`) and **Conditions**
  (`GET /deals/{id}/conditions`, `GET /conditions/{id}`). A download URL expires after five minutes.
  See [Files](/files).
- **Notes** on a contact and on a deal, both read and write.
- **Tags**: the two vocabularies (`GET /contact-tags`, `GET /deal-tags`), and assign and unassign on
  both parents.
- **Lenders** (`GET /lenders`).
- **`PATCH /deals/{id}`** for status, stage, assigned broker, and the closing and maturity dates.
  The API rejects a stage from a different pipeline, and a broker from a different brokerage.
- **`PATCH /tasks/{id}`** for the title, the body, the due date, and the pinned flag.
- **`PATCH /webhooks/{id}`** for the URL, the events, and the enabled flag. When you enable an
  endpoint again, the API clears its failure count.

**Added: [How the objects fit together](/model)**, a map of which id resolves through which
endpoint.

## 2026-08-03

**Added: `Idempotency-Key` on every `POST`.** Send the header to make a create safe to retry. The
API replays the first response instead of creating a second record. The header is optional, and a
request without it behaves as it did before. See [Idempotency](/idempotency).

**Breaking: the pagination envelope is now `snake_case`.** `pageSize` became `page_size`, and
`totalPages` became `total_pages`. They now match the `page_size` query parameter, and every other
field on the surface.

```diff
- "pagination": { "total": 412, "page": 1, "pageSize": 50, "totalPages": 9 }
+ "pagination": { "total": 412, "page": 1, "page_size": 50, "total_pages": 9 }
```

The old wrapper came from an internal helper that serves our own dashboard, and it would have kept
moving with that dashboard. We changed it on the day that we published the API, before anybody read
it.

**Changed: the reference now documents every field.** Behaviour did not change. The OpenAPI document
described about a quarter of its fields before. It now describes all of them.

**Changed: `success` is published as a plain boolean.** Behaviour did not change. The value is still
`true` on a success and `false` on an error, and the field says so. OpenAPI 3.0 has no `const`, so
the fixed value was a single-value `enum`, and the reference rendered that as an empty "Enum values"
chip on every response.

## 2026-08-03: beta

The first public release. It had 16 endpoints, across identity, contacts, leads, deals, tasks, and
webhooks. A key from **Settings > Developer** authenticated all of them.
