# Run an intake

An intake is the mortgage application a client fills in themselves. Over the API you can hand out a
link to start one, watch each applicant's progress as they type, read every answer, and approve the
intake or send it back for changes.

## The two links

|                | The standing link                               | A client's link                                    |
| -------------- | ----------------------------------------------- | -------------------------------------------------- |
| Who it is for  | Anybody. A website, an email signature, a card. | One named client.                                  |
| How many       | One for each broker.                            | One for each client, and more on request.          |
| Expires        | Never.                                          | Yes, and opening it moves the date later.          |
| Readable later | Yes, `GET /v1/intake-link`.                     | **No.** BrokerPlus keeps only a fingerprint of it. |

Because a client's link can't be read back, "send me the link I mailed her last week" has no answer.
Issue a new one instead: every link issued for the same intake works until it expires.

## The standing link

```sh
curl https://api.mos.getbrokerplus.ca/v1/intake-link \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": { "enabled": true, "url": "https://mos-portal.getbrokerplus.ca/i/alex-broker-3f9c21a8" }
}
```

`url` is `null` until the link has been turned on once. To turn it on or off:

```sh
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/intake-link \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
```

Turning it off stops new intakes but keeps the URL, so a link already printed on a card works again
the moment you turn it back on.

## Ask one client

The client must already be a contact. A deal is optional: leave `deal_id` out for someone who isn't
on a file yet, and BrokerPlus opens the deal when they send the intake back.

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contact_id": "cmscllt4h01xyp3iuq7dk2m8z", "delivery": "link" }'
```

```json
{
  "success": true,
  "data": {
    "object": "intake",
    "id": "cmsctw7kb0001p3x3zpyhkjv3",
    "contact_id": "cmscllt4h01xyp3iuq7dk2m8z",
    "deal_id": null,
    "co_applicant_contact_ids": [],
    "status": "in_progress",
    "delivered_at": null,
    "first_opened_at": null,
    "last_activity_at": "2026-10-01T15:04:00.000Z",
    "submitted_at": null,
    "approved_at": null,
    "created_at": "2026-10-01T15:04:00.000Z",
    "updated_at": "2026-10-01T15:04:00.000Z",
    "applicants": [
      {
        "contact_id": "cmscllt4h01xyp3iuq7dk2m8z",
        "name": "Jordan Client",
        "role": "primary",
        "submitted_at": null,
        "answered": 4,
        "total": 31,
        "unanswered": [
          {
            "section_key": "goal",
            "section_label": "Goal",
            "questions": [{ "key": "loan_purpose", "label": "Loan purpose" }]
          }
        ]
      }
    ],
    "url": "https://mos-portal.getbrokerplus.ca/qz8...",
    "url_expires_at": "2027-03-30T15:04:00.000Z"
  }
}
```

`delivery` decides how the client gets it:

- `link` (the default) returns the link in `url` and sends nothing. You deliver it yourself.
- `email` has BrokerPlus email each applicant their link now, from the broker's own mailbox. The
  response has `url` set to `null`. If the broker's mailbox isn't connected, the request returns
  `412 EMAIL_NOT_CONNECTED`. A key with the `no_sending` limit can't use `email`.

Add a second applicant with `co_applicant_contact_ids`. Each one gets their own link, and
`POST /v1/intakes/{id}/link?contact_id=...` issues another for one of them.

If the client already has an open intake, the request returns `409 DUPLICATE_INTAKE`. Find it with
`GET /v1/intakes?contact_id=...` instead of creating a second.

## Issue another link

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/link \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "url": "https://mos-portal.getbrokerplus.ca/h2k...",
    "expires_at": "2027-03-30T15:40:00.000Z"
  }
}
```

**The link is the client's credential.** Anyone who holds it can act as them, so send it to the
client and nowhere else.

## Follow an intake

```sh
curl "https://api.mos.getbrokerplus.ca/v1/intakes?status=submitted&updated_since=2026-09-30T00:00:00Z" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

`status` is `in_progress`, `submitted`, `approved`, or `needs_changes`. The timestamps tell the rest:
`delivered_at` is when the client first had it, `first_opened_at` when they first looked,
`submitted_at` when they sent it back, and `approved_at` when the broker accepted it.

Filtering by `contact_id` finds intakes where that person is a co-applicant, as well as their own.

To be told instead of polling, listen for the `intake.started` and `intake.submitted`
[webhooks](/webhooks).

## See each applicant's progress

Every intake has `applicants`: the primary client first, then each co-applicant. Answers save as the
client types, so progress moves before anyone submits.

```json
{
  "contact_id": "cmscllt4h01xyp3iuq7dk2m8z",
  "name": "Jordan Client",
  "role": "primary",
  "submitted_at": null,
  "answered": 18,
  "total": 31,
  "unanswered": [
    {
      "section_key": "income",
      "section_label": "Income",
      "questions": [{ "key": "employments", "label": "Employment" }]
    }
  ]
}
```

- `answered` and `total` count required questions only, the same count the client sees.
- `total` can move. Some answers add questions and some remove them: a client who refinances gets no
  down payment questions, for example.
- `unanswered` lists each section that still has a required question with no answer.
- A question for the whole household, such as the property or the lawyer, belongs to the primary
  client. A co-applicant doesn't owe it.
- An applicant's `submitted_at` is when that applicant sent their part. The intake's `submitted_at` is
  when the primary client sent the whole application.

## Read the answers

```sh
curl https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/answers \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": {
    "intake_id": "cmsctw7kb0001p3x3zpyhkjv3",
    "status": "submitted",
    "applicants": [
      {
        "contact_id": "cmscllt4h01xyp3iuq7dk2m8z",
        "name": "Jordan Client",
        "role": "primary",
        "submitted_at": "2026-10-03T19:12:00.000Z",
        "sections": [
          {
            "key": "goal",
            "label": "Goal",
            "answers": [
              {
                "key": "loan_purpose",
                "label": "Loan purpose",
                "kind": "field",
                "required": true,
                "answered": true,
                "value": "3",
                "value_label": "Purchase",
                "withheld": false
              }
            ]
          }
        ]
      }
    ]
  }
}
```

You get every question each applicant is asked, answered or not, the same view the broker has in
BrokerPlus. Answers are live: before the intake is submitted you see what the client has filled in so
far, so check `submitted_at` before you treat an answer as final.

- `value` is the answer as BrokerPlus stores it. For a choice question, `value` is the option code
  and `value_label` is the option name.
- A `repeater` answer is a list of rows, for example one row for each job.
- A `consent` or `signature` answer is `true` or `false`.
- The API never returns the Social Insurance Number (SIN). That answer has `withheld: true` and
  `value: null`, and `answered` tells you whether the client gave one.

## Read what the client said on a call

When a client answers intake questions on a call or in an email, BrokerPlus keeps each answer and
fills it in on the next intake the client is sent. `GET /v1/contacts/{id}/captured-answers` lists
them. Add `pending=true` for the answers still waiting for an intake: that is what the next one
will carry.

```json title="Response (shortened)"
{
  "success": true,
  "data": [
    {
      "object": "captured_answer",
      "id": "cmscap81k",
      "contact_id": "cmscllt4h01xyp3iuq7dk2m8z",
      "intake_key": "annual_income",
      "value": "90000",
      "heard_as": "about ninety thousand",
      "source": "call",
      "communication_id": "cmscom2b1",
      "occurred_at": "2026-10-01T15:00:00.000Z",
      "applied_at": null
    }
  ]
}
```

`heard_as` is what the client said, word for word. The client sees it beside the filled-in answer.
These answers are read-only.

## Read what the client was asked for

```sh
curl https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/items \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

An item is one thing on the client's portal other than a question: a document, a form to sign, a
consent, an identity check, or where the down payment comes from. The questions are on the answers
endpoint. Each item has a `key` and lists the applicants it is for. For each applicant you get:

- `doc_status`: where the checks on their file are, for example `verified` or `rejected`.
- `broker_stage`: the tray the broker put it in on the document board.
- `submitted_at`, and `dismissed_at` with `dismiss_reason` when they said they can't provide it.
- `uploads`: the files they sent for it, newest first.

An item that is only for one applicant, or that an earlier answer ruled out, lists only the people it
is for. For only what the client still owes on a deal, use `GET /v1/deals/{id}/document-requests`.

`GET /v1/intakes/{id}/uploads` lists every file the client sent. `GET /v1/intakes/{id}/uploads/{uploadId}`
returns one with a download URL that works for five minutes. A file the client replaced is not
listed. A key with the `no_documents` limit can't reach items or files.

## Accept a file

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/uploads/cmsu1a2b30001p3x3k9q8w7e6/stage \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stage": "verified", "folder": "income" }'
```

This is a drag on the broker's document board. `verified` accepts the file: BrokerPlus reads it and
copies it to the deal's documents, and `document_id` on the file then names the copy. `pending` and
`review` take it back. Nothing goes to the client.

To accept an item that has no file, for example a document the broker already has, send
`POST /v1/intakes/{id}/items/{key}/stage` with the applicant's `contact_id` and the `stage`.

To refuse a file, ask for changes with `uploads`, below.

## Review what the checks raised

`GET /v1/intakes/{id}/flags` lists what BrokerPlus's checks found on the intake, the most serious
first: an answer that disagrees with a document, a gap in income, a file that may not be genuine.
Filter with `status`: `active`, `resolved`, `overridden`, or `stale`. A flag with `audience`
`never_disclose` is for the broker only. Never pass it on to the client.

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/flags/cmsv7c2d10001p3x3m4n5b6v7/override \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Explained by the employer letter on file." }'
```

This is **Override** on the flag in BrokerPlus. The flag becomes `overridden`, and the reason is kept on
it and in the audit log. The reason needs at least 10 characters. Nothing goes to the client.

## Write notes on an intake

Brokers can leave notes for each other on one file or one item of a client's intake. The client
never sees them. `GET /v1/intakes/{id}/notes` lists them, newest first, replies included; filter
with `upload_id` or `item_key`.

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/notes \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "item_key": "t4", "body": "Ask for the 2025 slip as well." }'
```

Send `upload_id` to put the note on a file, or `item_key` to put it on an item. For a co-applicant's
item, add their `contact_id`. To reply, send `parent_id` alone: the reply goes where the note it
answers is. A reply cannot be replied to. A teammate mentioned in the note is told, as in BrokerPlus.
A note cannot be changed or deleted afterwards.

## Shape the income calculation

When a client is self-employed, BrokerPlus works out their income from their bank statements. The
broker decides what that calculation counts, on the stated income card.
`GET /v1/intakes/{id}/stated-income-scope` reads those choices: the dates it counts, the statements
and bank accounts left out, and the yearly income the client declared. `accounts` lists every bank
account on the client's statements, each with an `account_key`.

```sh
curl -X PATCH https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/stated-income-scope \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "date_range_start": "2025-10-01", "excluded_account_keys": ["RBC|9091"], "declared_annual_income": 85000 }'
```

A field you leave out keeps its value. `excluded_upload_ids` and `excluded_account_keys` replace the
whole list, so send `[]` to put everything back. BrokerPlus then works the income out again, which
takes a moment. Nothing goes to the client. An account number is only ever the last 4 digits.

### Take out or recategorize a transaction

`GET /v1/intakes/{id}/transactions` lists every transaction BrokerPlus read off the client's bank
statements, newest first. Each one says what BrokerPlus read it as (`category`, `subcategory`), what
the calculation counts it as (`effective_category`), and whether the broker took it out (`excluded`).
Filter with `q`, `upload_id`, `category`, `subcategory`, `applicant_role`, `date_from`, `date_to` and
`excluded`.

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/transactions/override \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "transaction_ids": ["cmsu1a2b30001p3x3k9q8w7e6:2026-02-09:950:2"], "excluded": true, "note": "Personal spending." }'
```

This changes up to 2000 transactions at once, as selecting rows on the stated income card does. A
field you leave out keeps each transaction's own value, so changing a category does not put back a
transaction the broker took out. An id you send that is not on the intake comes back in
`skipped_transaction_ids`. `DELETE /v1/intakes/{id}/transactions/{transactionId}/override` undoes the
broker's change. A transaction id contains `:`, so encode it in the path. Nothing goes to the client.

## Approve an intake

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/approve \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

This is **Approve** on the intake in BrokerPlus: BrokerPlus builds the submission package and locks
the answers. The response is the intake with `status` `approved`, and the client gets no message.
The intake must be `submitted` or `needs_changes`; otherwise the response is `409 WRONG_STATE`.

## Ask for changes

```sh
curl -X POST https://api.mos.getbrokerplus.ca/v1/intakes/cmsctw7kb0001p3x3zpyhkjv3/request-changes \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Two small fixes before I send this to the lender.",
    "changes": [
      { "question_key": "date_of_birth", "comment": "This date is in the future." }
    ]
  }'
```

This emails the client now. The intake becomes `needs_changes`, and each applicant with a reopened
answer gets an email with your `message` and a link to fix it, with your `comment` beside each
answer. The response is the intake. A key with the `no_sending` limit can't ask for changes.

- Use the `key` values from the answers endpoint for `question_key`.
- Set `contact_id` to reopen a co-applicant's answer. Leave it out for the primary client.
- The client must have submitted first. Before that the request returns `409 NOT_SUBMITTED`, because
  every answer is still open to them.
- A key that isn't on the intake, or a contact who isn't an applicant, returns `422 VALIDATION_ERROR`
  with each one named in `details.fields`.

To refuse a file the client sent, name it in `uploads` with an `upload_id` from the files list and a
`comment`. You can send `uploads` alone, or with `changes`. The client who sent each file gets your
comment and is asked for a new copy. The new copy replaces the refused one.

```json
{
  "message": "One page is missing.",
  "uploads": [{ "upload_id": "cmsu1a2b30001p3x3k9q8w7e6", "comment": "Please send the whole T4." }]
}
```

### Follow up on the changes you asked for

`GET /v1/intakes/{id}/change-requests` lists every answer and file you asked the client to fix, with
`status` `open` or `resolved`. Each one is `target` `answer`, with its `question_key`, or `upload`,
with its `upload_id`. None of these sends anything to the client:

- `PATCH /v1/intakes/{id}/change-requests/{crId}` with `comment` rewords it. The client sees the new
  words the next time they open their link.
- `POST .../resolve` marks it done, and `POST .../reopen` puts it back to `open`.
- `DELETE /v1/intakes/{id}/change-requests/{crId}` withdraws it.

An AI assistant can read an intake, its items, its files, its flags and its change requests, move
items and files on the board, override a flag, and reword, resolve or reopen a change request. It can't download a file, approve an intake, ask for changes or withdraw one. Those are the broker's
decisions.

## What you can't do yet

- Change what an intake asks for.
- Revoke a link. It stops working on its own when it expires.

If you need either, tell us and we'll tell you where it is in the queue.
