# Configure your workspace

Everything a broker sets up under **Settings** in BrokerPlus (custom fields, tags, lead stages,
pipelines, task categories, meeting types, hours, and the email signature) can also be set up
through the API. That makes it practical to copy a setup between brokerages, or to keep one in code.

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

## Who can change a setting

A key can change a setting only when its broker can change the same setting in BrokerPlus.

| Key                                      | Result                                           |
| ---------------------------------------- | ------------------------------------------------ |
| Not an admin, on an admin-only setting   | `403 FORBIDDEN`, and the message names the setting |
| Belongs to a view-only broker            | `403 VIEW_ONLY`                                  |
| Has the `read_only` limit                | `403 READ_ONLY_KEY`                              |
| Has the `leads_only` limit               | `403 KEY_LIMITED`                                |

Settings belong to one brokerage, so an id from another brokerage is `404 NOT_FOUND`. See
[Authentication](/authentication) for keys and their limits.

Billing, roles, invitations, team membership, brand settings, and integration credentials are never
in the API. Change those in BrokerPlus.

## Custom fields

A custom field is a question the brokerage adds to every contact or every deal (a lead uses the
contact fields). Only an admin's key can create, change, order, or archive one.

`POST /v1/custom-fields` creates a field, and BrokerPlus makes its `key` from the `label`. You use
that `key` to read and write answers on records.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/custom-fields \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"entity":"deal","label":"Lead Source","type":"select","options":["Google","Referral"],"default_value":"Referral"}'
```

```json
{
  "success": true,
  "data": {
    "object": "custom_field_definition",
    "id": "cm9f2k1x0000",
    "entity": "deal",
    "key": "lead_source",
    "label": "Lead Source",
    "type": "select",
    "options": ["Google", "Referral"],
    "section": "Custom Fields",
    "help_text": null,
    "default_value": "Referral",
    "position": 4,
    "created_at": "2026-10-07T14:02:11.000Z",
    "updated_at": "2026-10-07T14:02:11.000Z"
  }
}
```

`PATCH /v1/custom-fields/{id}` changes the `label`, `options`, `section`, or `default_value`. The
`key` and `type` are fixed, because every record's answers depend on them.

- `options` is the full list. An option you leave out is removed, and an option keeps its answers
  only if you send its label unchanged.
- Removing the default option sets the default to null.
- A new default applies only to records created after the change.

`POST /v1/custom-fields/reorder` takes one entity's field ids, first to last; fields you leave out
keep their place. `DELETE /v1/custom-fields/{id}` archives a field: it leaves every form, but
BrokerPlus keeps the answers. Creating a field with the same label and type brings it back, answers
and all.

## Tags

Contact tags and deal tags are two separate lists. Any key that can write can create, change, and
archive tags, because every broker can in BrokerPlus.

Create one with `POST /v1/contact-tags` or `POST /v1/deal-tags`. On a contact tag, `is_type: true`
shows it in the contact's Type row; deal tags don't take `is_type`.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/contact-tags \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Realtor","color":"amber","is_type":true}'
```

```json
{
  "success": true,
  "data": {
    "object": "tag",
    "id": "cm9f2m7q0000",
    "name": "Realtor",
    "color": "amber",
    "position": 3,
    "is_type": true,
    "created_at": "2026-10-07T14:05:40.000Z",
    "updated_at": "2026-10-07T14:05:40.000Z"
  }
}
```

`PATCH` changes the name or colour (`navy`, `amber`, `slate`, `red`, or a `#rrggbb` hex). `DELETE`
archives the tag and takes it off every record. Creating a tag with an archived tag's name restores
the tag, but not its place on those records.

To put a tag on a record or take it off, use `POST /v1/contacts/{id}/tags` and
`DELETE /v1/contacts/{id}/tags/{tagId}`, or the same paths under `/v1/deals`.

## Lead stages

Lead stages are the columns of the Leads board. A `leads_only` key can read them but not change
them.

- `POST /v1/lead-stages` adds a stage at the end of the board.
- `PATCH /v1/lead-stages/{id}` renames it; its leads stay put.
- `POST /v1/lead-stages/reorder` sets the order. Every id must be a live stage, or nothing changes.
- `DELETE /v1/lead-stages/{id}` deletes it and moves its leads to the stage in `move_to`, or to the
  first remaining stage. The board keeps at least one stage, so deleting the last one returns
  `409 LAST_STAGE`.

```bash
curl -X DELETE "https://api.mos.getbrokerplus.ca/v1/lead-stages/cm9f2n1a0000?move_to=cm9f2n1b0000" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": { "id": "cm9f2n1a0000", "deleted": true }
}
```

## Lead channels

A lead channel says how a lead found the brokerage, such as Google or a referral.

- `POST /v1/lead-channels` creates one.
- `PATCH /v1/lead-channels/{id}` renames or reorders it. Channels BrokerPlus made for every brokerage
  can move but can't be renamed (`400 SYSTEM_CHANNEL`).
- `DELETE /v1/lead-channels/{id}` archives it, and the leads and deals that had it are left with no
  channel.

## Referral partners

A referral partner is someone who sends the broker business, such as a realtor. In the API it is a
referral source with a contact behind it. The partner's figures are on
[`GET /v1/referral-partners`](/model#referral-partners).

`POST /v1/referral-sources` creates one. Send `contact_id` to use a contact you already have;
otherwise BrokerPlus makes a partner contact from `name`, `first_name`, `last_name`, `email`, and
`phone`.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/referral-sources \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Sam Realtor","first_name":"Sam","last_name":"Realtor","email":"sam@example.com","type":"realtor","company":"Example Realty"}'
```

```json
{
  "success": true,
  "data": {
    "object": "referral_source",
    "id": "cm9f2p4c0000",
    "name": "Sam Realtor",
    "position": 7,
    "type": "realtor",
    "company": "Example Realty",
    "contact_id": "cm9f2p4d0000",
    "contact_details": null,
    "notes": null,
    "last_contact_at": null,
    "next_follow_up_at": null
  }
}
```

`PATCH /v1/referral-sources/{id}` changes a partner, and a new `name`, `company`, email, or phone also
goes onto the partner's contact. If that contact's broker locked its details, you get
`403 CLIENT_DATA_LOCKED` and nothing changes. `DELETE /v1/referral-sources/{id}` archives the partner;
leads that named them lose their referral source, and the contact stays in the book.

### Who referred whom

A referral records the contact who sent a person to the broker, as Referred By on the contact page.
A person has one referrer. `POST /v1/contact-referrals` records one from `referrer_contact_id` and
`referred_contact_id`, with an optional `source` (for example `realtor` or `past_client`), `status`,
`note`, `deal_id`, and `occurred_at`. If the person already has a referrer, it is replaced and the
response is `200`.

`GET /v1/contact-referrals?referrer_contact_id=...` lists everyone one partner sent, with both
names. `PATCH /v1/contact-referrals/{id}` changes the status, kind, note, deal, or date; BrokerPlus
also sets `funded` itself when the linked deal funds. `DELETE /v1/contact-referrals/{id}` removes the
referral and keeps both contacts. A partner's totals are on `GET /v1/referral-partners`.

### Introductions to a partner

An introduction records a client or lead the broker sent to a partner: the other half of who
referred whom, and the `sent` count on the Partners page. `POST /v1/partner-introductions` records
one from `partner_contact_id` and `introduced_contact_id`, with an optional `deal_id`, `note`, and
`occurred_at`. A person is introduced to a partner once, so recording the same pair again changes the
one introduction. BrokerPlus sends nothing to the partner or the client.

`GET /v1/partner-introductions?partner_contact_id=...` lists everyone the broker sent to one partner,
with both names. `DELETE /v1/partner-introductions/{id}` removes the record and keeps both contacts.

### Partner firm notes

The brokerage can keep one note on each partner firm, usually to say which office its partners work
from. Every broker reads and writes the same notes, as on the Partners page.
`POST /v1/partner-company-notes` writes the note on a firm from `company` and `note`. Case is
ignored, so `RE/MAX` and `Re/Max` are one firm, and a firm that already has a note gets its text
replaced. `GET /v1/partner-company-notes?company=...` finds the note on a firm.
`PATCH /v1/partner-company-notes/{id}` changes the text, and `DELETE` removes the note.

### Partner roles

A partner role says what a contact does for the broker's clients: `realtor`, `lawyer`, `appraiser`,
`lender_contact`, `financial_planner`, `builder`, and so on, or a word the brokerage made up. A
contact with any role is a partner, and a contact can hold several.

`POST /v1/contacts/{id}/partner-roles` gives a contact a role, with an optional `title`. Adding a
role the contact already holds keeps the one role. `GET /v1/contacts/{id}/partner-roles` lists the
roles with their `title`, `website`, `calendar_url`, and `photo_url`.
`PATCH /v1/contacts/{id}/partner-roles/{roleId}` changes the title, website, or booking link; the
photo is uploaded in BrokerPlus. `DELETE /v1/contacts/{id}/partner-roles/{roleId}` takes a role off.
When the last role goes, the contact is no longer a partner, and it stays in the book.

### Partner hub links

A partner hub is one page where a referral partner sees every file they are on with the broker: the
client's first name, the city, and where the file is. The partner opens it from a link, with no
sign-in. Issue a link from the API and give it to the partner yourself, in a text or your own email.
BrokerPlus sends nothing.

`POST /v1/partner-hubs` opens the hub for a partner contact and issues a link in `link.url`. If the
partner already has a hub, you get that hub and a new link.

Open files are always on the hub. `show_funded` and `show_not_proceeding` choose which finished files
are on it: the files that funded, and the files that ended without funding. Both are `true` until
the broker turns one off. Send either one with `POST /v1/partner-hubs` to change it.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/partner-hubs \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact_id":"cm9f2p4d0000"}'
```

```json
{
  "success": true,
  "data": {
    "object": "partner_hub",
    "id": "cm9h1k2a0000",
    "contact_id": "cm9f2p4d0000",
    "partner_name": "Sam Realtor",
    "notify_stage_changes": false,
    "notify_funded": false,
    "show_funded": true,
    "show_not_proceeding": true,
    "revoked_at": null,
    "last_sent_at": null,
    "links": [{ "object": "partner_hub_link", "id": "cm9h1k2b0000", "hub_id": "cm9h1k2a0000", "channel": "copy", "recipient_email": null, "view_count": 0, "last_viewed_at": null, "created_at": "2026-10-09T15:00:00.000Z" }],
    "created_at": "2026-10-09T15:00:00.000Z",
    "updated_at": "2026-10-09T15:00:00.000Z",
    "link": {
      "object": "partner_hub_link",
      "id": "cm9h1k2b0000",
      "hub_id": "cm9h1k2a0000",
      "channel": "copy",
      "recipient_email": null,
      "view_count": 0,
      "last_viewed_at": null,
      "created_at": "2026-10-09T15:00:00.000Z",
      "url": "https://portal.mos.getbrokerplus.ca/r/Q2F0IGluIHRoZSBoYXQ"
    }
  }
}
```

BrokerPlus keeps only a fingerprint of each link, so you can read `url` only in the response that
issued it. For another link to a hub you already have, use `POST /v1/partner-hubs/{id}/link`. Every
link keeps working until the broker turns the hub off in BrokerPlus. Issuing a link turns a hub
that was off back on, the same as in BrokerPlus.

`GET /v1/partner-hubs` lists the hubs; `q` finds a partner by name. `GET /v1/partner-hubs/{id}` shows
one hub with the links that still work and how often the partner opened each one. A key with the
`leads_only` limit can't reach these routes.

## Client tracker

The client tracker is the list of stages a client sees on their hub, in the broker's own words, with
the day the file reached each one. The stages belong to a team, and every deal of the team shows the
same stages. A key works on the tracker of the team it acts in.

`GET /v1/tracker-stages` lists the stages in order. `POST /v1/tracker-stages` adds one at the end,
`PATCH /v1/tracker-stages/{id}` changes its words, and `POST /v1/tracker-stages/reorder` sets the
order. `DELETE /v1/tracker-stages/{id}` removes a stage from every deal, but BrokerPlus keeps the
dates: add a stage with the same name to bring it back with them. A stage's `key` never changes,
because automations name a stage by its key. Every client of the team sees a new, changed, or
reordered stage at once, so a key with the `no_sending` limit can't add, change, reorder, or seed
stages, or change the tracker note (`403 KEY_LIMITED`). It can still read and remove them.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/tracker-stages \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"Appraisal Booked","description":"The appraiser visits the home.","board_stage_keys":["appraisal"]}'
```

```json
{
  "success": true,
  "data": {
    "object": "tracker_stage",
    "id": "cm9h3t1a0000",
    "team_id": "cm9a1b2c0000",
    "key": "appraisal_booked",
    "label": "Appraisal Booked",
    "description": "The appraiser visits the home.",
    "client_note": null,
    "position": 5,
    "board_stage_keys": ["appraisal"],
    "created_at": "2026-10-09T15:00:00.000Z",
    "updated_at": "2026-10-09T15:00:00.000Z"
  }
}
```

`GET /v1/deals/{id}/tracker` shows one deal's tracker as the client sees it: each stage, the day the
deal reached it, and the note from the broker. When the tracker and the deal's pipeline stage
disagree, `drift` says which way. BrokerPlus changes neither, because the broker decides.

`POST /v1/deals/{id}/tracker/{stageId}/reach` marks a stage reached now, and returns `200` with the
deal's tracker. The client sees a reached stage, and reaching one can start automations that message
the client. So a key with the `no_sending` limit can't reach a stage (`403 KEY_LIMITED`), and if the
broker your key acts as may not contact clients, you get `403 FORBIDDEN`. `POST /v1/deals/{id}/tracker/{stageId}/retract` takes a stage
and every later stage back down at once, and sends nothing.

### Tracker links

Before partner hubs, a broker could give a realtor or a lawyer a link that showed one deal's
tracker. BrokerPlus no longer makes these links: give the partner a
[partner hub](#partner-hub-links) instead. The links already handed out keep working until the
broker takes them back.

`GET /v1/deals/{id}/tracker/links` lists the links on a deal that still work, newest first, with who
each one is for and how often it was opened. A link does not include its address, because BrokerPlus
never kept it. `POST /v1/deals/{id}/tracker/links/{linkId}/revoke` takes one back: it stops working
at once, nobody is told, and the link stays on record with `revoked_at` set.

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/deals/cmsd8f2k10001p3x3abcd1234/tracker/links/cmsl4n7q20001p3x3wxyz5678/revoke \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

## Pipelines and stages

A pipeline is a board of deals, and each stage is a column on it. A key reaches the pipelines of its
broker's team, and any broker on the team who can change deals can change them. A stage that stops
automated messages also needs permission to change automations.

`POST /v1/pipelines` creates a pipeline. Without `stages`, it starts with the standard ones. A stage
can have `requirements`: what a deal must have before it can move in.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/pipelines \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Commercial","allowed_deal_types":["purchase"],"stages":[{"key":"new","label":"New"},{"key":"submitted","label":"Submitted","requirements":[{"kind":"field","key":"ownership.brokerOfRecord"}]}]}'
```

```json
{
  "success": true,
  "data": {
    "object": "pipeline",
    "id": "cm9p1a2b0000",
    "name": "Commercial",
    "description": null,
    "allowed_deal_types": ["purchase"],
    "position": 3,
    "key": null,
    "broker_user_id": "cm9u7x0b0000",
    "stages": [
      {
        "object": "pipeline_stage",
        "id": "cm9s1c3d0000",
        "key": "new",
        "label": "New",
        "position": 0,
        "requirements": []
      },
      {
        "object": "pipeline_stage",
        "id": "cm9s1c3d0001",
        "key": "submitted",
        "label": "Submitted",
        "position": 1,
        "requirements": [
          {
            "kind": "field",
            "key": "ownership.brokerOfRecord",
            "label": null,
            "required": null,
            "mode": "cumulative",
            "deal_types": []
          }
        ]
      }
    ],
    "created_at": "2026-10-07T14:02:11.000Z",
    "updated_at": "2026-10-07T14:02:11.000Z"
  }
}
```

The stages are shortened here. `PATCH /v1/pipelines/{id}` changes a pipeline, and
`POST /v1/pipelines/reorder` sets the team's order. A new deal lands on the first pipeline that
accepts its type, so the order also decides where new deals go.

Each stage endpoint returns the whole pipeline:

- `POST /v1/pipelines/{id}/stages` adds a stage. Sending the `key` of an archived stage restores it.
- `PATCH /v1/pipelines/{id}/stages/{stage_id}` changes a stage; `requirements` replaces the full list.
- `POST /v1/pipelines/{id}/stages/reorder` sets the order, with every stage included.
- `DELETE /v1/pipelines/{id}/stages/{stage_id}` archives a stage.

A requirement applies to its stage and every later stage. Set `mode` to `this_stage` to apply it to
its stage only, or to `optional` to ask for it without stopping the move. This works the same for a
`field`, a `checkpoint`, and a `custom_field`, which names one of your deal custom fields by its key.

BrokerPlus checks every requirement first: a field must be one it knows, a participant one of your
seats, a checkpoint a step on the team's checklist, and a custom field one of your live deal custom
fields. Otherwise you get `400 VALIDATION_ERROR` and nothing changes.

Deals protect the boards they sit on:

- A stage with active deals can't be deleted: `409 STAGE_IN_USE`, with the count. Move them first.
- A pipeline with deals can't be deleted: `409 PIPELINE_IN_USE`. To archive the deals along with it,
  as a broker does when they confirm the delete in BrokerPlus, send
  `DELETE /v1/pipelines/{id}?archive_deals=true`.
- A broker's last pipeline can't be deleted: `409 LAST_BOARD`.

## Participant seats

A seat is a participant that a team's deals keep a place for before anybody fills it: a listing
realtor, a lawyer, an underwriter. A stage requirement and an automation recipient name a seat by
its `kind`. A curated role is the role itself, for example `lawyer`. The brokerage's own word is
`label:` and the word, for example `label:Listing Realtor`.

- `GET /v1/participant-seats` lists the seats of the key's active team, in deal card order.
- `POST /v1/participant-seats` adds a seat. Send `role` or `role_label`, not both. A word that the
  brokerage already uses in a different case becomes the same seat. A second seat of the same kind
  gets `409 DUPLICATE`.
- `PATCH /v1/participant-seats/{id}` pins or unpins a seat, or changes `default_contact_id`.
- `DELETE /v1/participant-seats/{id}` removes a seat. The people already in it stay on their deals.
  A stage that requires the seat blocks this: `409 SEAT_REQUIRED`, with each stage in
  `details.stages`.

A pinned seat shows on each deal that the team opens after the pin. Open deals do not change. To
change one deal only, use `POST /v1/deals/{id}/participant-seats/{seatId}/adopt` or `.../dismiss`.
`GET /v1/deals/{id}/participant-seats` lists the seats of the deal's team, with `on_deal` true for
each seat that the deal shows.

Only a member of a seat's team can change it. Other keys get `404 NOT_FOUND`. The MCP server has a
tool for each of these operations.

## Task categories

Every task has a category. BrokerPlus has built-in ones, and a team can add up to 25 of its own. Only
the team's broker or a brokerage admin can change them; other keys get `403 FORBIDDEN`.

`POST /v1/task-categories` creates one. Its name must be unique on the team, or you get
`409 DUPLICATE`.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/task-categories \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Appraisals"}'
```

```json
{
  "success": true,
  "data": {
    "object": "task_category",
    "key": "custom:cm9k4t2a0000",
    "id": "cm9k4t2a0000",
    "name": "Appraisals",
    "built_in": false,
    "team_id": "cm9t1b7c0000",
    "position": 0,
    "hidden": false
  }
}
```

- `PATCH /v1/task-categories/{id}` renames it.
- `DELETE /v1/task-categories/{id}` deletes it, and its tasks go back to their built-in category.
- `POST /v1/task-categories/hidden` hides built-in categories. Send the full list, such as
  `{"hidden":["compliance"]}`, or an empty list to show them all. `admin` can't be hidden. The
  response is every category, with `hidden` set on each.

## Note types

Every deal note and contact note has a kind, such as `underwriting` or `call`. Deals and contacts
have separate lists. BrokerPlus has built-in kinds, and the broker's team can add, rename, recolour,
or remove kinds. A change applies to the team of the key's broker, and a broker who is not on a team
gets `400 NO_TEAM`.

`GET /v1/note-types?scope=deal` lists the kinds that a deal note can have. Send a `slug` as `type`
when you add a note.

`POST /v1/note-types` adds a kind. BrokerPlus makes the `slug` from the `name`, and the `slug` never
changes. A `template` is the text that a new note of the kind starts with in BrokerPlus.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/note-types \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope":"deal","name":"Appraisal","color":"amber"}'
```

```json
{
  "success": true,
  "data": {
    "object": "note_type",
    "scope": "deal",
    "slug": "appraisal",
    "name": "Appraisal",
    "color": "amber",
    "template": null,
    "built_in": false
  }
}
```

- `PATCH /v1/note-types/{scope}/{slug}` changes the `name`, `color`, or `template`.
- `DELETE /v1/note-types/{scope}/{slug}` removes a kind. Its notes keep their `type`. You cannot
  remove `general`.

## NPS alert rules

When a client answers a Net Promoter Score (NPS) survey, BrokerPlus checks the score against the
brokerage's alert rules. Every active rule whose range holds the score runs: it alerts brokers in
the app or by email, and it can add a task on the client's contact. Ranges can overlap, so one score
can run two rules. The rules belong to the brokerage, and any broker can change them.

`GET /v1/nps-alert-rules` lists the rules. A brokerage with no rules gets three starter rules on the
first read: 0 to 6, 7 to 8, and 9 to 10. `POST /v1/nps-alert-rules` adds one. Only `name`,
`min_score`, and `max_score` are required.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/nps-alert-rules \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Call within the hour","min_score":0,"max_score":4,"channels":["in_app","email"],"task_title":"Call this client today"}'
```

```json
{
  "success": true,
  "data": {
    "object": "nps_alert_rule",
    "id": "cm9n2p8r0000",
    "name": "Call within the hour",
    "min_score": 0,
    "max_score": 4,
    "notify_owner": true,
    "broker_ids": [],
    "channels": ["in_app", "email"],
    "task_title": "Call this client today",
    "task_body": null,
    "task_assignee_user_id": null,
    "task_due_in_days": 2,
    "is_active": true,
    "position": 3,
    "created_at": "2026-10-09T14:02:11.000Z",
    "updated_at": "2026-10-09T14:02:11.000Z"
  }
}
```

- `PATCH /v1/nps-alert-rules/{id}` changes the fields you send. `min_score` must stay at or below
  `max_score`, or you get `400 VALIDATION_ERROR`.
- `DELETE /v1/nps-alert-rules/{id}` deletes the rule.
- A broker in `broker_ids` or `task_assignee_user_id` must be in the brokerage, or you get
  `404 NOT_FOUND`.

## Meeting types

A meeting type is a booking link clients use to book time with a broker. A key can manage its own
broker's meeting types and the shared ones of any team its broker can write for, but never another
broker's.

`POST /v1/meeting-types` creates one. Without `team_id`, the key's broker hosts it.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/meeting-types \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Discovery call","duration_minutes":30,"buffer_after_minutes":10,"min_notice_minutes":240,"questions":[{"label":"Purchase or refinance?","type":"choice","options":["Purchase","Refinance"],"required":true}]}'
```

```json
{
  "success": true,
  "data": {
    "object": "meeting_type",
    "id": "cm9f3a7k0000",
    "name": "Discovery call",
    "slug": "discovery-call",
    "duration_minutes": 30,
    "owner_user_id": "cm9f0b2c0000",
    "owner_name": "Alex Broker",
    "team_id": null,
    "booking_url": "https://example.com/alex-broker/discovery-call",
    "min_notice_minutes": 240,
    "buffer_before_minutes": 0,
    "buffer_after_minutes": 10,
    "availability_hours": null,
    "questions": [
      { "id": "4f1c2e0a", "label": "Purchase or refinance?", "type": "choice", "options": ["Purchase", "Refinance"], "required": true }
    ],
    "position": 3
  }
}
```

The response is shortened; `GET /v1/meeting-types` describes every field.
`PATCH /v1/meeting-types/{id}` changes one, and the change applies to bookings made after it:

- `availability_hours` sets hours for this meeting type only (send all seven days), or null to use
  the broker's booking hours.
- `questions` is the full list; send a question's `id` to keep it.
- `form_fields` sets each field to `required`, `optional`, or `hidden`. Fields you leave out keep
  their mode.
- `reminder_minutes` and `reminder_channels` change client reminders. Changing them sends nothing.

`POST /v1/meeting-types/reorder` sets the order on the booking page. `DELETE /v1/meeting-types/{id}`
removes the meeting type and turns off its link; appointments already booked stay on the calendar,
and clients aren't told. To stop new bookings but keep the link, send `enabled: false` instead.

Booking a client into a meeting type emails them now. A key with the `no_sending` limit cannot book.
See [Scheduling](/model#scheduling).

## Working hours

`GET /v1/me/hours` and `PATCH /v1/me/hours` read and change the key's own broker's time zone and
hours. A key can't change anyone else's.

- `business_hours` are when the broker's BrokerPlus phone line rings through.
- `booking_hours` are when clients can book them. Null means bookings use `business_hours`.

Each schedule has one entry per day, `0` (Sunday) to `6` (Saturday), and each day up to six windows
in minutes after midnight, in the broker's time zone. A day with no windows is closed.

```bash
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/me/hours \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"timezone":"America/Toronto","booking_hours":[{"day_of_week":0,"windows":[]},{"day_of_week":1,"windows":[{"start_minute":540,"end_minute":720},{"start_minute":780,"end_minute":1020}]},{"day_of_week":2,"windows":[{"start_minute":540,"end_minute":1020}]},{"day_of_week":3,"windows":[{"start_minute":540,"end_minute":1020}]},{"day_of_week":4,"windows":[{"start_minute":540,"end_minute":1020}]},{"day_of_week":5,"windows":[{"start_minute":540,"end_minute":900}]},{"day_of_week":6,"windows":[]}]}'
```

```json
{
  "success": true,
  "data": {
    "timezone": "America/Toronto",
    "business_hours": null,
    "booking_hours": [
      { "day_of_week": 0, "windows": [] },
      { "day_of_week": 1, "windows": [{ "start_minute": 540, "end_minute": 720 }, { "start_minute": 780, "end_minute": 1020 }] },
      { "day_of_week": 2, "windows": [{ "start_minute": 540, "end_minute": 1020 }] },
      { "day_of_week": 3, "windows": [{ "start_minute": 540, "end_minute": 1020 }] },
      { "day_of_week": 4, "windows": [{ "start_minute": 540, "end_minute": 1020 }] },
      { "day_of_week": 5, "windows": [{ "start_minute": 540, "end_minute": 900 }] },
      { "day_of_week": 6, "windows": [] }
    ]
  }
}
```

Blocked days are days clients can't book the broker, such as a vacation. `POST /v1/me/blocked-days`
blocks one day or a range of up to 90, `GET /v1/me/blocked-days` lists them, and
`DELETE /v1/me/blocked-days/{date}` unblocks one. Appointments already booked on a blocked day stay.

## Email signature

`GET /v1/me/email-signature` returns the key's broker's signature as HTML, and
`PATCH /v1/me/email-signature` replaces it. A key changes only its own broker's signature.

BrokerPlus strips scripts, styles, and other markup email clients don't show, and returns the HTML
it kept. Send an empty string to remove the signature.

```bash
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/me/email-signature \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<p><strong>Alex Broker</strong></p><p>Example Mortgage Group</p>"}'
```

```json
{
  "success": true,
  "data": { "html": "<p><strong>Alex Broker</strong></p><p>Example Mortgage Group</p>" }
}
```

Changing it sends nothing. The new signature goes on the broker's next emails, from every inbox that
doesn't have its own signature.

## Team note

Each team has one shared note. Every member sees it on their overview and can change it.
`GET /v1/teams/{id}/note` reads it, and `PATCH /v1/teams/{id}/note` replaces the whole note. Only a
member of the team can do either; for any other team the answer is `404 NOT_FOUND`. Get the team ids
from `GET /v1/teams`.

```bash
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/teams/cm9a1t0e0000/note \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"<p>Lender rate sheets moved to Tuesdays.</p>"}'
```

```json
{
  "success": true,
  "data": {
    "object": "team_note",
    "team_id": "cm9a1t0e0000",
    "team_name": "Alex Broker's Team",
    "body": "<p>Lender rate sheets moved to Tuesdays.</p>",
    "updated_at": "2026-10-09T15:00:00.000Z"
  }
}
```

The body is HTML. BrokerPlus removes unsafe markup and returns what it kept. Send an empty string to
clear the note. Changing the note notifies nobody, and a mention in it does not notify the teammate.

## Assistant instructions

A broker can give their assistant standing instructions, such as "Sign off with my first name" or
"Never move a deal to Approved before the appraisal is in". Each instruction has a `topic` that
says when the assistant applies it. These are the same instructions as Assistant settings in
BrokerPlus, and the assistant follows a change from its next run.

An instruction belongs to the broker's team, or to the whole brokerage. Only an admin can add,
change, or delete a brokerage instruction, and `editable` tells you whether the key's broker can.
A topic holds at most 8 instructions for a team, and 8 for the brokerage.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/agent-instructions \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"topic":"client_writing","body":"Sign off with my first name only."}'
```

```json
{
  "success": true,
  "data": {
    "object": "agent_instruction",
    "id": "cm9a1i0n0000",
    "scope": "team",
    "team_id": "cm9a1t0e0000",
    "topic": "client_writing",
    "title": null,
    "body": "Sign off with my first name only.",
    "enabled": true,
    "editable": true,
    "created_by_user_id": "cm9a1u0s0000",
    "created_at": "2026-10-10T15:00:00.000Z",
    "updated_at": "2026-10-10T15:00:00.000Z"
  }
}
```

- `GET /v1/agent-instructions` lists them, with an optional `topic` filter.
- `PATCH /v1/agent-instructions/{id}` changes the fields you send. `enabled: false` switches one
  off and keeps it.
- `DELETE /v1/agent-instructions/{id}` deletes one.

## Team chat

Team chat is where brokers on the same team talk to each other: a conversation between two people,
or a group. A key sees only the conversations that its broker is in, and a team chat message never
goes to a client.

1. Find who the broker can message with `GET /v1/direct-threads/roster`. It lists the active members
   of each team the broker is on.
2. Start a conversation with `POST /v1/direct-threads`. One teammate in `user_ids` opens the
   conversation with them, or returns the one the broker already has. Two or more, or a `title`,
   start a new group.
3. Post with `POST /v1/direct-threads/{id}/messages`. The other members see the message at once.

```bash
curl -X POST https://api.mos.getbrokerplus.ca/v1/direct-threads/cmv2z0q.../messages \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"The appraisal is in. Can you call the lender?"}'
```

```json
{
  "success": true,
  "data": {
    "object": "direct_message",
    "id": "Vq3h0e...",
    "thread_id": "cmv2z0q...",
    "sender_user_id": "cm9a1u0e...",
    "body": "<p>The appraisal is in. Can you call the lender?</p>",
    "body_text": "The appraisal is in. Can you call the lender?",
    "created_at": "2026-10-10T15:00:00.000Z"
  }
}
```

`GET /v1/direct-threads` lists the conversations, the newest message first, and `unread` is `true`
when a teammate wrote after the broker last read. `GET /v1/direct-threads/{id}/messages` lists the
messages, newest first. Reading messages does not mark them read: `POST /v1/direct-threads/{id}/read`
does that, and the other members then see how far the broker has read. A conversation the broker is
not in answers `404 NOT_FOUND`.
