# How the objects fit together

The [endpoint reference](/reference) lists every field. This page is the map: what each object is,
how the objects hang together, and which id you follow to get from one to the next.

This API is in beta. Any endpoint can change in a breaking way without notice, so watch the
[changelog](/changelog).

## Your brokerage

Everything a key can reach belongs to one brokerage, and the key acts as one broker in it.
[`GET /me`](/reference#tag/identity) tells you which. A key never reaches another brokerage, and it
never does more than the broker who made it. See [Authentication](/authentication).

**Users** are the brokers and staff. [`GET /users`](/reference#tag/users) lists them with their
licences. Any field that names a person (`owner_id` on a contact, `assigned_broker_id` on a deal,
`assignee_user_id` on a task, `author_id` on a note) is a user id that `GET /users/{id}` resolves.
A user carries identity only: name, email, role, and whether the account is active. Phone numbers
and booking handles stay out, because several of them work like credentials.

Most ids come with a readable name beside them, such as `assigned_broker_name` on a deal or
`assignee_name` on a task. Store the id and show the name; the name can change. Every record also
has an `object` field (`"deal"`, `"contact"`, `"task"`) so you can tell types apart in one list.

**Teams** group brokers. [`GET /teams`](/reference#tag/teams) lists them, and each has exactly one
owner. What a key sees depends on the teams its broker belongs to.

## Contacts and leads

A **contact** is a person or a company in the book; `kind` tells you which. `types` says what they
are to you (borrower, referral partner, realtor), and one contact can be several at once.

Contacts carry more than the main `email` and `phone`:

- **Addresses** through [`/contacts/{id}/addresses`](/reference#tag/contacts): the broker's own
  addresses for the person (home, work, mailing, cottage). Exactly one is primary. The first one you
  add becomes primary, and `is_primary: true` on another moves it. The residence history on a
  mortgage application is a separate, read-only record.
- **Methods** through [`/contacts/{id}/methods`](/reference#tag/contacts): extra emails and phones,
  fax numbers, and social profiles. A social profile returns a `url` built from what the broker
  typed.
- **Relationships** through [`GET /contacts/{id}/relationships`](/reference#tag/contacts), worded
  from that contact's side.

To find a person before you create one, send `email` or `phone` to `GET /contacts` or `GET /leads`.
Email ignores case, phone compares the last 10 digits, and both check every email and phone on the
contact. No match is an empty list. [Capture leads](/capture-leads) walks through this.

### Duplicates, merge, and archive

[`GET /contacts/duplicates`](/reference#tag/contacts) lists sets of contacts that share an email, a
phone, or a name. To merge one, pick the contact to keep and send the others:

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/contacts/cmsctz1fa0003p3x3k2l7q9wd/merge \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Idempotency-Key: merge-cmsctz1fa" \
  -H "Content-Type: application/json" \
  -d '{"duplicate_ids": ["cmsd0b2kq0007p3x3a9f1c2re"]}'
```

Deals, notes, documents, and history move to the contact you keep. A merge can't be undone. After
it, `GET /contacts/{id}` on a merged id returns `410 CONTACT_MERGED` with `details.merged_into_id`,
so you can replace the id you stored.

`POST /contacts/{id}/archive` takes a contact out of the lists and sets `archived_at`. The contact
keeps its history, `GET /contacts/{id}` still returns it, and a later import or lead with the same
email finds it. `POST /contacts/{id}/unarchive` puts it back.

### Leads

A **lead** is an inbound enquiry that is not a deal yet. Every lead has a contact, and the name,
email, and phone live on that contact. `status` is `open`, `archived`, or `converted`.
[`POST /leads/{id}/convert`](/reference#tag/leads) starts a deal, and the lead then carries it as
`converted_deal_id`.

## Custom fields

A brokerage keeps its own **custom fields** on contacts and deals.
[`GET /custom-fields`](/reference#tag/custom-fields) lists them with their `key`, type, and options.
A field with a `default_value` starts with that answer on each new record. A lead has no fields of
its own: it uses its contact's, and you can set them with `custom_fields` on `POST /leads`.

Filter a list by a field with `custom_field[<key>]=<value>` on `GET /contacts`, `GET /leads`, or
`GET /deals`, for example `GET /v1/leads?custom_field[lead_source]=Google`. To create or change the
fields themselves, see [Configure your workspace](/workspace#custom-fields).

## Contact statistics

[`GET /contacts/{id}/statistics`](/reference#tag/contacts) says how a contact stands in the book. The
figures are computed on each read from the deals your key can see, and a deal counts once for each
borrower on it.

Funded volume uses the commission's funded amount when the broker recorded one, and the
application's loan amount otherwise. A funded deal with neither counts in
`funded_deals_without_amount` and adds nothing, so while that count is above zero the volume is a
floor, not a total. Year-to-date uses the brokerage's calendar year in `timezone`. A contact is a
repeat client once two of their deals have funded, and `last_touch` is the latest note, message, or
deal change.

## Pipelines, stages, and deals

A **deal** is a mortgage file. It sits in one place on one pipeline:

- `board_id` is the **pipeline**, resolved by [`GET /pipelines/{id}`](/reference#tag/pipelines).
- `stage_id` is a **stage** on that pipeline. Stages come inside the pipeline, in board order.
- `stage_name` is a copy of the stage label, so a list view needs no second call.

Every brokerage sets up its own pipelines, so read stages from `GET /pipelines` rather than
hardcoding a name or an id. A pipeline BrokerPlus created has a `key` (for example `historical`,
where funded and past deals go); look it up by `key`, not by name. A stage with `exclude_from_book`
holds paid-out mortgages, and a deal on a stage with `stops_automated_outreach` gets no automated
messages. `allowed_deal_types` says which deals land on a pipeline by default; it doesn't stop a
deal from moving there.

`POST /deals` opens a deal for the key's broker. Without `board_id`, it lands on the broker's default
pipeline for its `type`, on the first stage. If you name a pipeline, it must belong to the broker's
team and `stage_id` must be on it. Attach borrowers with `contacts`, each with a role.

`PATCH /deals/{id}` can move a deal only to a stage on **its own pipeline**. The API refuses a stage
from another pipeline, because the deal would drop out of every board view.
`GET /deals/{id}/stage-requirements` tells you what a stage needs before you move a deal into it.

### Copy, archive, and restore a deal

`POST /deals/{id}/duplicate` makes a new deal with the same people and primary application, on the
first stage of the same pipeline. `carry_participants: false` copies only the primary applicant,
`type` gives the copy another deal type, and `archive_original: true` archives the original once the
copy exists. Submissions, funding, compliance, and documents are not copied.

`DELETE /deals/{id}` archives a deal, and `POST /deals/{id}/restore` brings it back. Tasks the
archive cancelled stay cancelled.

### Flags

A flag paints a deal's card red on the board. `GET /deals/{id}/flags` lists the flags that are up,
each with a plain-words `reason` and a stable `reason_code`. `raised_by` is `broker` for the one flag
a broker raises by hand, `automation` for a Pulse automation, or `brokerplus` for BrokerPlus itself.

`POST /deals/{id}/flags` raises the broker's flag (send it again to change the words), and
`POST /deals/{id}/flags/{flagId}/clear` takes it down. Other flags are read-only: clearing one
returns `409 SYSTEM_FLAG`. Flags are internal; no client sees them.

### The deal's checklist

Each team keeps a checklist of steps for its files, such as "appraisal ordered" or "commitment
signed". `GET /deals/{id}/checkpoints` lists the team's steps with their value on the deal: `true`
for done, `false` for not done, and null when nobody has set it.
`PATCH /deals/{id}/checkpoints/{checkpointId}` sets one step, and `value: null` clears it.

### Find a deal by an id from another system

A deal imported from Velocity, Filogix, or Finmo keeps its old id in `external_references`, and the
lender's file number sits in `lender_file_ref`. Look one up with
`GET /deals?external_reference=<id>` (add `source` to name the system) or
`GET /deals?lender_file_ref=<number>`. Both match the whole value exactly, and an empty list means no
deal has that id.

## What hangs off a deal

| Sub-resource         | Endpoint                                    | What it is                                                       |
| -------------------- | ------------------------------------------- | ---------------------------------------------------------------- |
| Mortgage application | `GET /deals/{id}/application`               | The loan: amount, term, amortization, rate, payment, and ratios. |
| Subject property     | `GET /deals/{id}/property`                  | The property that secures the loan.                              |
| Applicants           | `GET /deals/{id}/applicants`                | Everyone on the application, with jobs and income.               |
| Financials           | `GET /deals/{id}/financials`                | What the applicants own and owe, and the down payment.           |
| Other properties     | `GET /deals/{id}/other-properties`          | Real estate the applicants already own.                          |
| Terms history        | `GET /deals/{id}/application/terms-history` | Each change to the loan terms, with who and when.                |
| Documents            | `GET /deals/{id}/documents`                 | The files on the deal. See [Documents](/files).                  |
| Conditions           | `GET /deals/{id}/conditions`                | What a lender needs before the deal funds.                       |
| Participants         | `GET /deals/{id}/participants`              | The borrowers and the partners on the deal.                      |
| Deal team            | `GET /deals/{id}/team`                      | The users who work the deal, by role.                            |
| Commission           | `GET /deals/{id}/commission`                | What the brokerage earns, and how it is split.                   |
| Consents             | `GET /deals/{id}/consents`                  | What each party authorized.                                      |
| Notes and tags       | `GET /deals/{id}/notes`, `/deals/{id}/tags` | Commentary and your own labels.                                  |
| Stage history        | `GET /deals/{id}/stage-history`             | Every stage move, newest first.                                  |
| Activity             | `GET /deals/{id}/activity`                  | The trail of messages, calls, notes, and changes.                |

## Participants and partners

A **participant** is a contact on a deal with a role, and `is_partner` splits the roles in two:

- **Borrower** roles are on the application: `primary_applicant`, `co_applicant`, `guarantor`, and
  `power_of_attorney`.
- **Partner** roles are professionals on the file: `realtor`, `lawyer`, `appraiser`,
  `lender_contact`, `underwriter_contact`, `credit_assistant`, `insurance`, `financial_planner`,
  `accountant`, `builder`, or `other` (with your own word in `role_label`).

A contact holds one role per deal. Add and remove participants, and change a partner's role, through
[`/deals/{id}/participants`](/reference#tag/deals). A borrower's role belongs to the application and
doesn't change here.

A partner on a deal is not the same as a partner in the book: `types` on a contact says the person
refers business to you, while the role on a deal says what they do on one file.

### Referral partners

A deal's `referral_partner` is who sent you the deal. Set it with
`POST /deals/{id}/referral-partner`, using `contact_id` for a contact or `name` for someone who isn't
one. A contact becomes a partner the first time you name them.

[`GET /referral-partners`](/reference#tag/referral-partners) reads the partner book with the same
figures as the Partners page. A partner's `id` is their contact id, or `source:` plus an id for a
name-only partner; pass it as it is to `GET /referral-partners/{id}`. People count on the day they
were referred, money on the day the deal funded. The book covers every team, but a key limited to
some teams sees only the people and deals those teams can read. To create or change partners, see
[Configure your workspace](/workspace#referral-partners).

## Teams and members

A **team** is one broker's crew. The broker owns it, and everyone else is a **member**, with the
brokerage's own `title` for the seat (underwriter, assistant, submitting broker).
[`GET /teams/{id}/members`](/reference#tag/teams) reads the roster. Membership is set up in
BrokerPlus, not through the API.

## Commission

A **commission** is what the brokerage earns on a funded deal, one per deal, read and changed through
[`/deals/{id}/commission`](/reference#tag/deals). Every figure is what the broker typed; the API
doesn't derive a fee from a rate sheet.

- The top level holds the lender, funded amount, finder's fee, bonus, buydown, and gross.
- A **line item** says where part of the money comes from. When line items exist, `gross_amount` is
  their sum less the buydown, and the API keeps it that way.
- A **split** allocates the gross to a payee: a broker, the house, a franchise, or a referral
  partner. It says who is owed a share, not that it was paid.

Commission has its own access rule. The key's broker can read it as the deal's broker, as a brokerage
admin, or as a team member with the `view_commission` grant. Anyone else gets
`403 COMMISSION_ACCESS`, even on a deal they can change. Changes post to the brokerage's connected
accounting system, and `revision` goes up on each one.

## Consents and communication preferences

A **consent** records that one party on one deal authorized one thing: a credit pull, a lender
submission, privacy processing, electronic delivery, or a mortgage protection plan transfer. Read and
add them through [`/deals/{id}/consents`](/reference#tag/deals), and take one back with
`POST /deals/{id}/consents/{consentId}/revoke`. Consents are never deleted, so the file always shows
what the brokerage held at the time. When the client agreed to written text, `text` and
`text_version` keep it as it read then. A `credit_pull` consent lapses 30 days after it is given.
A consent is a record only; it doesn't start a pull or a submission.

A **communication preference** is the client's standing on commercial email and SMS. It belongs to
the person, not a deal: [`GET /contacts/{id}/communication-preferences`](/reference#tag/contacts).
Email has three categories (`all_commercial`, `marketing`, `equity_reports`), and withdrawing from
`all_commercial` stops every commercial message. SMS has one switch. No record means `implied`, the
default for a client in the book.

`PATCH` on the same path takes one action per category: `opt_out` records a withdrawal, `opt_in`
clears one without recording express consent, and `express` records express consent with your
`source`. A **suppression** (a hard bounce, a complaint, a STOP reply) shows as `suppressed`, and the
API doesn't change it.

## Applicants and financials

Every rate and ratio here is a percent; see [Units and formats](/units).

An **applicant** is one person on the application, with `role` `primary`, `co_applicant`, or
`guarantor`. Each carries their jobs and any other income. Their name and contact details live on
`contact_id`. The API never returns a social insurance number.

**Financials** are the money on the deal: assets, liabilities, and down payment sources. The API
never returns an account number. A liability with `from_credit_bureau: true` came from a credit
report, not from the applicant. Consumer report data has its own redistribution rules, so check
your obligations before you store or forward it.

### Calculated figures on the application

`GET /deals/{id}/application` returns the figures BrokerPlus calculates from the rows, the same ones
the broker sees:

- `gds`, `tds`, and `ltv` (null when there isn't enough data), and `ratio_qualifying_rate`, the rate
  those ratios use.
- `total_loan_amount`: the loan plus the insurance premium, when the premium is added to the loan.
- `insurance_premium`, `insurance_premium_rate`, and `insurance_premium_added_to_loan` for mortgage
  default insurance.
- `stated_fees`: the fees the lender stated.

The property adds `appraised_value` and `appraisal_date`. These figures are read-only; change the
rows they come from.

### The rest of the application

The application, the property, and their rows carry the same details the broker fills in on the
deal. The application has the loan, charge, and repayment types, the buyer type, the legal and
closing costs, and the purchase stage. The property has heating, water, parking, zoning, size,
purchase history, taxes, and rental costs. An applicant has an honorific, a suffix, sex, and a
language. A job has the employer's address and the business type. A liability has its debt, charge,
and payout types, plus `property_id` when it is the mortgage on another property.

`PATCH /deals/{id}/application` and `PATCH /deals/{id}/property` change these, as do the row
endpoints below. The first application update creates the application on a deal that has none. A
coded field takes the numeric `code` it returns on read. `product_code` and `tenure` are read-only,
because the origination system sets them.

### Edit the application one row at a time

Each applicant, job, income source, address, phone number, asset, liability, and other property
has its own `POST`,
`PATCH`, and `DELETE`. A request changes one row and leaves the rest alone, and BrokerPlus
recalculates the totals and ratios straight away.

| Row             | Endpoint                                                               |
| --------------- | ---------------------------------------------------------------------- |
| Applicant       | `/deals/{id}/applicants/{applicantId}`                                 |
| Job and its pay | `/deals/{id}/applicants/{applicantId}/employments/{employmentId}`      |
| Other income    | `/deals/{id}/applicants/{applicantId}/income-sources/{incomeSourceId}` |
| Address         | `/deals/{id}/applicants/{applicantId}/addresses/{addressId}`           |
| Phone number    | `/deals/{id}/applicants/{applicantId}/phones/{phoneId}`                |
| Asset           | `/deals/{id}/assets/{assetId}`                                         |
| Liability       | `/deals/{id}/liabilities/{liabilityId}`                                |
| Other property  | `/deals/{id}/other-properties/{propertyId}`                            |

To create a row, `POST` to the path without the last id. To add an applicant, first put the contact
on the deal as a borrower with `POST /deals/{id}/participants`, then send their `contact_id`.

This adds a job that pays $4,250 twice a month:

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/deals/cm7x2q1k80001/applicants/cm7x2q1k80007/employments \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "employer_name": "Lakeshore Freight",
    "job_title": "Dispatcher",
    "started_on": "2021-03-15",
    "incomes": [{ "income_type": 1, "payment_frequency": 24, "amount": 4250 }]
  }'
```

An amount is per period, not per year. In a `PATCH` to a job, `incomes` is the full list: include
the `id` of each line to keep, and leave out each line to remove. A few rows push back:

- A liability from a credit report is read-only: `409 FROM_CREDIT_BUREAU`.
- You can't remove the primary applicant: `400 PRIMARY_APPLICANT`.
- An applicant must be on the deal as a borrower: `400 NOT_A_BORROWER`.

An applicant's addresses are their address history: one `current` address, each earlier home as a
`previous` address with its `started_on` and `ended_on`, and a `correspondence` address when their
mail goes somewhere else. Lenders ask for about three years of history. An applicant's phone numbers
are the ones the lender sees, and they are separate from the contact's own phone numbers.

### Credit notes

`GET /deals/{id}/credit-notes` lists the broker's own notes on the credit of the people on the deal,
one per person. `PATCH /deals/{id}/credit-notes/{contactId}` writes one, as the credit panel on the
deal does. The first write creates it. These are the broker's words only: no credit score and no
credit report goes out over the API, and a score the broker entered in BrokerPlus is kept as it is.

### Lender commitments

When the lender sends its commitment, BrokerPlus reads the terms from the PDF: the amount, the
rate, the term, the payment, the fees, and the prepayment and portability terms. The broker checks
them against the PDF and corrects what was read wrong. Only reviewed terms go on the cost of
borrowing disclosure that the client signs.

- `GET /deals/{id}/commitments` lists what was read, newest first. Each commitment has its `terms`,
  with the broker's corrections applied, and the `cost_of_borrowing` worked out from them. A value
  the PDF does not state is `null`. `terms.sources` says which page each value came from.
- `POST /deals/{id}/commitments` reads a PDF. With no body, it reads the PDF the lender sent with its
  latest decision. For a lender that sends none, upload the commitment as a document and send its id
  in `document_id`. Reading the same PDF again replaces its terms and clears the review.
- `POST /deals/{id}/commitments/{commitmentId}/review` marks the terms as checked. Send corrections
  in `edits`, for example `{ "edits": { "interest_rate": 4.79 } }`. They replace the corrections of
  an earlier review.

Rates are percents (see [Units](/units)). Nothing here sends anything to the client.

### Value history of the deal's property

`GET /deals/{id}/property-valuations` lists each valuation of the property the deal's mortgage is
on, newest first, with its estimate, its assessment, and its source. It is the home value trend on
the client's equity report, and BrokerPlus adds to it each month while the client gets reports. A
broker's own value, set with `property_value` on the opportunity, is not in this list.

### Creditor life insurance

`GET /deals/{id}/life-insurance` returns the deal's creditor life insurance: whether the broker
offered it, what the client said in `outcome`, and where the application is with the insurer in
`status`. `POST /deals/{id}/life-insurance/outcome` records the client's answer, as the insurance
panel on the deal does. Sending the application or asking for a quote shares the client's file with
the insurer under their consent, so the broker does that in BrokerPlus.

### Valuation history of an other property

`GET /deals/{id}/other-properties/{propertyId}/valuations` lists each valuation of the property,
newest first, with its estimate, its assessment, and its source. `change_12mo` shows how the estimate
changed over the last year, as a percent. It is `null` until there are two estimates to compare.

`POST /deals/{id}/other-properties/{propertyId}/refresh-valuation` asks the valuation service for a
new estimate, the same as the refresh button in BrokerPlus. The request returns `202` and the
valuation runs in the background. The property must have a street address, or the API returns
`400 NO_ADDRESS`. Send an `Idempotency-Key`: a retry with the same key does not ask again.

### Debt consolidation analysis

A debt consolidation analysis compares three ways to pay out the client's other debts: a refinance,
a home equity line of credit, and a reverse mortgage. It is the same analysis as the Debts tab in
BrokerPlus. It reads:

- Each debt on the deal that has a `debt_type`. Set `debt_type` when you add a debt with
  `POST /deals/{id}/liabilities`. A debt with no `debt_type` is not in the analysis.
- The other properties on the deal.
- The balance and the property value on the deal's opportunity. Set the value with
  `PATCH /opportunities/{id}` if BrokerPlus has no estimate.

`POST /deals/{id}/consolidation-analyses` runs the analysis and saves it. To replace the default rate
for an option, send `new_mortgage_rate_pct`, `heloc_rate_pct`, `reverse_mortgage_rate_pct`, or
`rental_heloc_rate_pct`, as a percent. BrokerPlus sends nothing to the client.

An analysis never changes. `GET /deals/{id}/consolidation-analyses` lists them newest first, so the
first one is the deal's current analysis. In each analysis, `status_quo` is the client's position if
nothing changes, `options` lists each option that BrokerPlus compared, and
`recommended_option_index` points to the recommended option. Every rate and the loan-to-value ratio
are percents.

### Terms history

`GET /deals/{id}/application/terms-history` lists each change to the rate, term, amortization, and
other loan terms, oldest first, with the old value, the new value, who changed it, and when. Use
`updated_since` to read only the newer changes.

## Conditions

A **condition** has two statuses. `status` is your team's answer; `lender_status` is the lender's,
and a condition your team considers done can still be outstanding with the lender. `source` says who
asked for it.

You change `status` with `POST /conditions/{id}/satisfy`, `POST /conditions/{id}/waive`, or
`PATCH /conditions/{id}`. `lender_status` belongs to the lender, and BrokerPlus refreshes it on each
decision poll.

`POST /deals/{id}/conditions` adds a condition the way the deal's Conditions tab does, and files a
task to decide it that completes when the condition is satisfied or waived. `assignee_role` says who
owes it (`broker`, `borrower`, `lawyer`, `credit_assistant`, `realtor`, `insurer`, or your own value),
and `assignee_contact_id` names the person when they are in your book. To explain open conditions to
someone on the deal in plain language, use `POST /deals/{id}/condition-requests`; it sends the letter
at once. See [Ask a client for something](/client-asks).

`documents` lists the documents attached to a condition as what answers it. They go to the lender
with the condition. `POST /conditions/{id}/documents` attaches more, as the attach on the Conditions
tab does. Each document has to be on the condition's deal. Attaching changes nothing else: satisfy
the condition when it is done.

## Scheduling

A **meeting type** is a booking link a broker shares with clients. `GET /meeting-types` lists the
key's broker's meeting types and their teams' shared ones. `GET /availability` returns the open start
times of one meeting type, calculated the way the booking page does: each host's hours in their own
time zone, with split shifts, blocked days, buffers, minimum notice, and calendar busy times.

`POST /appointments/book` books a contact into a time that `GET /availability` returned.
`POST /appointments/{id}/reschedule` moves a booking to another open time with the same host, and
`POST /appointments/{id}/cancel` cancels it. All three act now, email the client, and return the
booking. A key with the `no_sending` limit cannot use them. If the time is taken first, the call
returns `409` and nothing is sent. `GET /appointments/{id}/booking` reads the booking, including
after a move or a cancellation.

`/appointments` itself is the broker's calendar. Creating, changing, or deleting an appointment there
sends nothing to a client. To change meeting types, hours, and blocked days, see
[Configure your workspace](/workspace#meeting-types).

## Submissions

A **submission** is a deal a broker sent to a lender, with the lender and status. `source` says what
sent it:

- `brokerplus`: BrokerPlus sent it. Read the lender's answers through
  `GET /submissions/{id}/decisions`.
- `finmo`: the brokerage's connected Finmo account sent it, and BrokerPlus mirrors it. The lender's
  answer is in `lender_status`, and the requested terms are in `loan_amount`, `interest_rate`, and
  `term_months`. A Finmo submission has no decisions endpoint, and `attempt` is null.

A submission with `simulated: true` was a test that never reached a lender, so leave it out of
reports. The API reads submissions; it never sends one.

## Opportunities

An **opportunity** is a mortgage your brokerage tracks for a renewal or a refinance, with the
balance, the penalty to break it, and the savings from a move. `POST /opportunities` adds one by hand,
the same as Add Client on the book: BrokerPlus links the client by `email` or creates the contact,
and makes a deal for the mortgage. Each call creates one, so send an `Idempotency-Key`.

`effective_penalty` is the penalty that applies: the greater of `ird_penalty` and
`three_month_penalty`, unless the broker entered one from a lender payout statement. A timestamp in
`outreach_unsubscribed_at` means the client opted out of equity reports, and nothing else.

Where the broker typed over a figure BrokerPlus had, such as `comparison_rate` or
`property_bedrooms`, the opportunity carries the broker's figure. `best_option_pin` is the option the
broker chose to show the client, or null.

### Find opportunities

`GET /opportunities` takes the same filters as the book in BrokerPlus, and you can combine them.
This request finds the variable-rate mortgages that mature in the first quarter of 2027, soonest
first, and leaves out the ones the broker dismissed:

```sh
curl -G https://api.mos.getbrokerplus.ca/v1/opportunities \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -d maturity_from=2027-01-01 \
  -d maturity_to=2027-03-31 \
  -d product_type=variable,arm \
  -d dismissed=false \
  -d sort=maturity_asc
```

| Filter                            | Returns only                                                              |
| --------------------------------- | ------------------------------------------------------------------------- |
| `maturity_from`, `maturity_to`    | The mortgages that mature in this window. Both days are included.         |
| `dismissed`                       | `true`: the dismissed ones. `false`: the ones still on the book.          |
| `archived`                        | `true`: the mortgages whose term ended. `false`: the live ones.           |
| `opportunity_type`                | `renewal`, `pre_renewal`, `ird`, or `both`.                               |
| `product_type`                    | `fixed`, `variable`, `arm`, or `heloc`.                                   |
| `start_year`                      | The mortgages that started in these years, for example `2021,2022`.      |
| `assigned_broker_id`              | The mortgages on deals assigned to this broker.                           |
| `monthly_reports_enabled`         | `true`: the clients enrolled in equity reports. `false`: the rest.        |

Separate several values with commas. `sort` takes `maturity_asc`, `maturity_desc`,
`last_client_contact_desc`, or `last_client_contact_asc`; without it, the most recently changed
opportunity is first. A sync with `updated_since` always reads in `updated_at` order, so it can't
take `sort`. The filters work with a sync.

### Correct what the mortgage carries

BrokerPlus didn't arrange these mortgages, so the balance, rate, and payment are only as current as
the last statement. `PATCH /opportunities/{id}` corrects them. A deal doesn't carry its
opportunity's id; find it with `GET /opportunities?deal_id=cm7x2q1k80001` (there is at most one).

```sh
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/opportunities/cmsctw7kb0001p3x3zpyhkjv3 \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "balance": 383250, "interest_rate": 5.25 }'
```

```json
{
  "success": true,
  "data": {
    "object": "opportunity",
    "id": "cmsctw7kb0001p3x3zpyhkjv3",
    "balance": 383250,
    "balance_as_of": "2026-10-05T18:20:00.000Z",
    "balance_is_manual": true,
    "interest_rate": 5.25,
    "prime_adjustment": null,
    "effective_penalty": 4180,
    "renewal_savings": 9240
  }
}
```

Use `interest_rate` for a fixed rate and `prime_adjustment` for a variable one, where `-0.5` means
prime minus 0.50%. A balance you send becomes the new anchor: BrokerPlus pays it down from that day,
neither the nightly recalculation nor an import overwrites it, and `balance_is_manual` reads `true`.
Send null to hand it back. Penalties, savings, and the target rate follow from these figures, so one
request can change several fields. The original principal, maturity date, and amortization belong to
the deal, not here.

### Act on an opportunity

These do the same as the menu on an opportunity in BrokerPlus:

| Request                             | What it does                                                              |
| ----------------------------------- | ------------------------------------------------------------------------- |
| `POST /opportunities/{id}/convert`  | Starts a new working deal with the borrowers and mortgage facts.          |
| `POST /opportunities/{id}/dismiss`  | Takes the mortgage off the book and stops its equity reports and signals. |
| `POST /opportunities/{id}/restore`  | Puts a dismissed mortgage back.                                           |
| `POST /opportunities/{id}/snooze`   | Hides it until `until`, and reminds the broker that morning.              |
| `POST /opportunities/{id}/unsnooze` | Ends the snooze.                                                          |

Dismiss, restore, and unsnooze are safe to repeat. Convert creates a deal every time (a client can
refinance twice), so send an `Idempotency-Key`; the response is the new deal. A deal on a stage that
locks it refuses everything except convert with `403 DEAL_LOCKED`.

The equity reports about a client are at `GET /equity-reports?contact_id=...`. See
[Equity reports](/equity-reports).

## Notes

A note has the same shape on a contact and on a deal. Notes thread one level deep: a note can carry a
`parent_id`, and the API refuses a reply to a reply rather than flattening it. `pinned` is what the
app shows at the top.

`PATCH /contacts/{id}/notes/{noteId}` and `PATCH /deals/{id}/notes/{noteId}` change the words, the
pin, or the kind. A pin alone doesn't change `updated_at`. Anyone who can edit the contact or deal
can edit its notes, as in BrokerPlus. `DELETE` removes the note and its replies for good.

Task notes work the same at `PATCH /tasks/{id}/notes/{noteId}`, without a kind. Only the author can
delete a task note; anyone else gets `403 NOT_AUTHOR`.

A new note has the kind `general` unless you send `type`. The kinds are at
[Note types](/workspace#note-types).

### Mentions, reactions, and files

These work the same on a contact note and a deal note. The paths below use deals.

- **Mentions.** To mention a teammate, put `<span data-mention="USER_ID">@Name</span>` in the body.
  `GET /deals/{id}/notes/mentionable` lists the teammates that a note can mention. Each one gets a
  notification, as in BrokerPlus. `mentions` on the note lists who was named.
- **Reactions.** `POST /deals/{id}/notes/{noteId}/reactions` with `{"emoji":"👍","active":true}` adds
  the reaction of the key's broker, and `"active":false` takes it off. Sending it again changes
  nothing. `reactions` on the note sums them per emoji.
- **Files.** `POST /deals/{id}/notes/{noteId}/attachments` attaches a PDF of at most 25 MB, as
  `multipart/form-data` in a `file` field or as JSON with `file_name` and `content_base64`.
  `attachments` on the note lists the files, and
  `GET /deals/{id}/notes/{noteId}/attachments/{attachmentId}` gives a download link that works for
  five minutes. A key with the `no_documents` limit gets `403` on both.

## Drafts

A draft is a message written for the broker to review. `POST /contacts/{id}/drafts` files an `sms`
or `email` draft into the review queue of the key's broker with status `pending_review`, exactly like
a draft BrokerPlus writes. The broker reads it in BrokerPlus and sends, edits, or rejects it.

Use a draft when your system writes the words and a person should read every one before a client
does. To send a draft as it is written, use `POST /drafts/{id}/send`. To send a message without a
review, use `POST /contacts/{id}/messages`. Both send now, from the broker's connected mailbox or
phone number, and a key with the `no_sending` limit gets `403`. If you only want the broker to do
something, create a task instead.

```bash
curl https://api.mos.getbrokerplus.ca/v1/contacts/cm7x2q1k80001/drafts \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "sms", "body": "Hi Sam, your approval came through today. Call me when you have a minute." }'
```

```json
{
  "success": true,
  "data": {
    "object": "draft",
    "id": "cm7y8r4mf0009",
    "contact_id": "cm7x2q1k80001",
    "deal_id": null,
    "channel": "sms",
    "status": "pending_review",
    "subject": null,
    "body": "Hi Sam, your approval came through today. Call me when you have a minute.",
    "recipients": [{ "contact_id": "cm7x2q1k80001", "name": "Sam Client" }],
    "sender_user_id": "cm7usr9f30002",
    "scheduled_at": null,
    "communication_id": null,
    "created_at": "2026-09-22T14:05:11.000Z"
  }
}
```

The response is shortened here. An `sms` draft has no `subject` and at most 1600 characters. An
`email` draft needs a `subject`.

### Follow a draft after you file it

`GET /drafts` lists the drafts your key filed, newest first, and `GET /drafts/{id}` reads one. Filter
with `status`, `channel`, `contact_id`, `deal_id`, or `updated_since`. Drafts the broker wrote in
BrokerPlus aren't listed.

| `status`         | What it means                                           |
| ---------------- | ------------------------------------------------------- |
| `pending_review` | Waiting in the broker's review queue.                   |
| `scheduled`      | Set to go out at `scheduled_at`.                        |
| `sent`           | Sent. `communication_id` is the message the client got. |
| `discarded`      | The broker rejected it.                                 |
| `failed`         | A scheduled send didn't go out.                         |

The broker can edit a draft before it goes, so `subject` and `body` show what the client got.

### Change, schedule, or discard a draft

`PATCH /drafts/{id}` changes a draft that waits for review, and `POST /drafts/{id}/discard` throws it
away. `POST /drafts/{id}/schedule` sets it to go out at `send_at`, and
`POST /drafts/{id}/unschedule` stops it. To draft a reply to a message instead of a new message, see
[Reply to a client](/mail#reply-to-a-client).

## Tags

Contacts and deals have **separate tag vocabularies**: `GET /contact-tags` and `GET /deal-tags`. They
share a shape, not a namespace, so a deal tag means nothing on a contact.

Putting a tag on a record and taking it off are both idempotent. Adding the same tag twice leaves one,
and removing a tag that isn't there still succeeds. To create or change tags, see
[Configure your workspace](/workspace#tags).

## Lenders

[`GET /lenders`](/reference#tag/lenders) lists the lenders your brokerage can use. `source` says
where each comes from:

- `brokerplus`: the brokerage's own catalogue, with `is_prime`, `is_alternative`, and `is_private`.
- `finmo`: mirrored from the brokerage's connected Finmo account. Finmo folds the product line into
  the name, so "First National - Prime" and "First National - Excalibur" are two lenders.
  `lender_type` carries Finmo's classification.

A lender with `under_maintenance: true` can't take a submission right now. A submission's
`lender_id` resolves against the lender with the same `source`; send `source=brokerplus` or
`source=finmo` to list one side. Rate sheets and compensation aren't published.

## Tasks

Tasks are an **action queue**, not a general to-do list. Each has a `type` for the kind of work and a
`priority` that orders the list, and it can link to a deal, a contact, or both. BrokerPlus files many
tasks on its own.

You can create, change, assign, complete, cancel, and reopen tasks, and read and write their notes
(which a client never sees). A new task takes the team of its deal or contact, or the broker's own
team. An assignee must be eligible for that team and deal, by the same rule as BrokerPlus, or the API
returns `INVALID_ASSIGNEE`. A task with no assignee waits until somebody takes it.

`POST /tasks/bulk` completes, cancels, or reopens up to 200 tasks at once and returns `updated_ids`
and `skipped_ids`. A task already in that state is skipped, and so is an approval task, which only
the broker decides (cancel or reopen one directly and you get `409 APPROVAL_TASK`).
`POST /tasks/batch` creates or updates up to 100 tasks; see [Idempotency and batch](/idempotency).
