# Log calls from your phone system

Put a call from any phone system or recorder in the BrokerPlus call log. It shows up on the
contact and the deal with its transcript, and BrokerPlus writes the recap, just like a call the
broker made from BrokerPlus.

## Before you start

BrokerPlus already connects to Dialpad and Quo with no code: an admin turns them on in
**Settings > Integrations**, and calls flow in on their own. This guide is for everything else, for
example Plaud, Grain, Savo, or a second Dialpad company.

- A call lands in the call log of the broker who created the key. To log each broker's calls, use a
  key each broker creates.
- Get your contacts into BrokerPlus first. BrokerPlus finds a call's contact by the client's phone
  number.

## What happens when you send a call

When you send a call to `POST /v1/calls`, BrokerPlus:

1. **Finds the contact.** It uses `contact_id` if you send one. Otherwise it matches the client's
   number: `from` on an `inbound` call, `to` on an `outbound` one. A number that matches two
   contacts matches neither.
2. **Finds the deal.** It uses `deal_id` if you send one. Otherwise it picks the contact's most
   recently updated deal.
3. **Files the call** in the broker's call log and on the contact and the deal.
4. **Writes a recap** if the call has a transcript and you didn't send a `summary`, and fills in the
   application answers the client gave on the call. Send your own `summary` and BrokerPlus keeps it
   instead of writing one.
5. **Sends the `call.logged` webhook.**

## Log a call

<CodeTabs syncKey="lang">

```bash title="curl"
curl -X POST https://api.mos.getbrokerplus.ca/v1/calls \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "direction": "inbound",
    "provider": "Plaud",
    "external_id": "rec_7f3a91",
    "from": "+1 587 555 1234",
    "started_at": "2026-10-06T15:30:00Z",
    "duration_seconds": 312,
    "transcript_segments": [
      { "start_seconds": 0, "end_seconds": 4.5, "speaker": "client", "text": "Hi, it is Jordan about the renewal." },
      { "start_seconds": 4.5, "end_seconds": 9, "speaker": "broker", "speaker_name": "Alex Broker", "text": "Happy to help, Jordan." }
    ]
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/calls", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    direction: "inbound",
    provider: "Plaud",
    external_id: "rec_7f3a91",
    from: "+1 587 555 1234",
    started_at: "2026-10-06T15:30:00Z",
    duration_seconds: 312,
    transcript: "Jordan: Hi, it is Jordan about the renewal.\nAlex Broker: Happy to help, Jordan.",
  }),
});
const { data: call } = await res.json();
```

</CodeTabs>

Send the words either as plain `transcript` text or as timed `transcript_segments`. The response
is `201`:

```json
{
  "success": true,
  "data": {
    "object": "call",
    "id": "cmcall7q2...",
    "direction": "inbound",
    "status": "completed",
    "contact_id": "cmcontact...",
    "contact_name": "Jordan Client",
    "deal_id": "cmdeal...",
    "deal_name": "Client - Renewal",
    "user_id": "cmuser...",
    "from": "+1 587 555 1234",
    "to": null,
    "started_at": "2026-10-06T15:30:00.000Z",
    "duration_seconds": 312,
    "provider": "Plaud",
    "external_id": "rec_7f3a91",
    "recording_url": null,
    "has_recording": false,
    "transcript": "Client: Hi, it is Jordan about the renewal.\nAlex Broker: Happy to help, Jordan.",
    "transcript_segments": [
      { "start_seconds": 0, "end_seconds": 4.5, "speaker": "client", "speaker_name": null, "text": "Hi, it is Jordan about the renewal." },
      { "start_seconds": 4.5, "end_seconds": 9, "speaker": "broker", "speaker_name": "Alex Broker", "text": "Happy to help, Jordan." }
    ],
    "summary": null,
    "created_at": "2026-10-07T14:02:11.000Z",
    "updated_at": "2026-10-07T14:02:11.000Z"
  }
}
```

`summary` is `null` at first because BrokerPlus writes the recap a moment later. Read the call
again with `GET /v1/calls/{id}` to pick it up.

## Send each call once

Phone systems resend a webhook when they don't get a quick answer, so always send `external_id`
with the phone system's own call id. A second request with the same `provider` and `external_id`
returns the call that's already logged, with `200` instead of `201`, and logs nothing new. Case and
punctuation in `provider` don't matter: `Dialpad` and `dialpad` are the same system.

Send the call once the transcript is ready. BrokerPlus can't add a transcript to a call that's
already in the log, so if your phone system produces the transcript after the call ends, wait for
it.

## Example: relay a second Dialpad company

BrokerPlus connects to one Dialpad company per brokerage. This small server relays a second
company's calls: Dialpad posts a call event when a call ends, and the server sends it on to
BrokerPlus.

Dialpad's event fields can change, so check them against Dialpad's current documentation, and
verify each event's Dialpad signature before you read it.

```ts title="dialpad-relay.ts"
const API = "https://api.mos.getbrokerplus.ca/v1";

// Dialpad sends the call event here when a call ends.
Bun.serve({
  port: 8080,
  async fetch(req) {
    const event = await req.json(); // Check the Dialpad signature first.
    if (event.state !== "hangup" && event.state !== "missed" && event.state !== "voicemail") {
      return new Response("ignored");
    }

    const res = await fetch(`${API}/calls`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        direction: event.direction === "inbound" ? "inbound" : "outbound",
        provider: "Dialpad",
        external_id: String(event.call_id),
        from: event.direction === "inbound" ? event.external_number : event.internal_number,
        to: event.direction === "inbound" ? event.internal_number : event.external_number,
        started_at: new Date(event.date_started).toISOString(),
        duration_seconds: Math.round((event.duration ?? 0) / 1000),
        status: event.state === "hangup" ? "completed" : event.state,
      }),
    });

    // A 5xx or a 429 tells Dialpad to send the event again. The external_id makes that safe.
    return new Response(null, { status: res.ok ? 200 : 503 });
  },
});
```

## Read the call log

- `GET /v1/calls` lists the calls the broker can see, newest first. Filter by `contact_id`,
  `deal_id`, or `direction`, and use `updated_since` to sync (see [Sync](/sync)).
- `GET /v1/calls/{id}` returns one call.
- Subscribe to `call.logged` to hear about each new call. See [Webhooks](/webhooks).

The log includes calls on BrokerPlus lines and from connected phone systems; `provider` tells you
where each one came from.

## Read recorded meetings

Video meetings the BrokerPlus notetaker sat in, and Roam meetings, are on the Meetings tab, apart
from the call log.

- `GET /v1/meetings` lists them, newest first, with BrokerPlus's `summary`. Filter by `deal_id` or
  `contact_id`, and use `updated_since` to sync.
- `GET /v1/meetings/{id}` adds the transcript line by line, who was in the meeting and how long each
  person spoke, BrokerPlus's follow-ups in `action_items`, and the video platform's own summary
  when it writes one.

- `POST /v1/meetings` sends the notetaker into a Zoom, Google Meet or Microsoft Teams call, now or
  at `join_at`, as the broker your key acts as. Name a `deal_id` to file the meeting on a deal.
  Everybody in the call sees it join, so a key with the `no_sending` limit is refused it.
- `POST /v1/meetings/{id}/stop` takes the notetaker out of the call before it ends. A Roam meeting
  is stopped from inside Roam.
- `POST /v1/meetings/{id}/sync` asks the video platform for the transcript and summary again, for a
  meeting that ended without them. They land on the meeting within a few minutes.

The recording itself is not on the API.

## Call lists

A call list is a round of calls the broker works one row at a time. Each row is a person to call,
with the outcome of the call and a note. BrokerPlus calls nobody and sends nothing: the list records
what the broker did.

- `POST /v1/call-lists` makes a list from contacts or deals. A deal puts its primary applicant on the
  list.
- `POST /v1/call-lists/import` makes a list from rows of names, numbers, and emails, for example a
  spreadsheet you read yourself. Each row is matched to a contact by email, then by number. A row
  that matches nobody stays on the list only, unless you send `add_to_contacts: true`.
- `POST /v1/call-lists/{id}/entries` adds contacts, deals, or people typed in by name. A person
  already on the list keeps their outcome and note.
- `GET /v1/call-lists/{id}/entries` lists the rows, in the order the broker works them. Filter by
  `status`.
- `PATCH /v1/call-lists/{id}/entries/{entry_id}` records an outcome: `called`, `no_answer`,
  `follow_up`, `done`, or back to `pending`. Any outcome can follow any other.
- `DELETE /v1/call-lists/{id}/entries/{entry_id}` takes a person off the list, and
  `DELETE /v1/call-lists/{id}` deletes the list. The contacts stay.

```bash
curl https://api.mos.getbrokerplus.ca/v1/call-lists/cmlist0001/entries/cmentry0001 \
  -X PATCH \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "follow_up", "note": "Call back after the 15th." }'
```

```json
{
  "success": true,
  "data": {
    "object": "call_list_entry",
    "id": "cmentry0001",
    "call_list_id": "cmlist0001",
    "contact_id": "cmcontact0001",
    "deal_id": null,
    "name": "Sam Client",
    "email": "sam@example.com",
    "phone": "+14165550134",
    "status": "follow_up",
    "note": "Call back after the 15th.",
    "called_at": "2026-10-09T15:00:00.000Z",
    "called_by_user_id": "cmuser0001",
    "called_by_name": "Alex Broker",
    "created_at": "2026-10-08T14:00:00.000Z",
    "updated_at": "2026-10-09T15:00:00.000Z"
  }
}
```

`called_at` and `called_by_user_id` record when the row first left `pending` and who worked it. They
stay when the outcome changes again, and clear when the row goes back to `pending`.

## Call scripts

A call script is the list of questions a broker reads on a call, in sections. It is the same script
the broker sees in the Scripts pane. A question fills nothing in the application: it is a question
to ask, with an optional note for whoever runs the call.

- `GET /v1/call-scripts` lists the scripts, and `GET /v1/call-scripts/{id}` returns one.
- `POST /v1/call-scripts` makes a script on the broker's team.
- `PATCH /v1/call-scripts/{id}` changes the fields you send. `sections` replaces every section.
- `DELETE /v1/call-scripts/{id}` retires a script. A call that already used it still names it.

```bash
curl https://api.mos.getbrokerplus.ca/v1/call-scripts \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renewal check-in",
    "estimated_minutes": 10,
    "sections": [
      {
        "key": "today",
        "label": "Where they are today",
        "questions": [
          { "key": "lender", "prompt": "Who is your mortgage with now?" },
          { "key": "plans", "prompt": "Any plans to move in the next five years?", "note": "Ask about a cottage too." }
        ]
      }
    ]
  }'
```

```json
{
  "success": true,
  "data": {
    "object": "call_script",
    "id": "cmscript0001",
    "name": "Renewal check-in",
    "description": null,
    "estimated_minutes": 10,
    "sections": [
      {
        "key": "today",
        "label": "Where they are today",
        "description": null,
        "questions": [
          { "key": "lender", "prompt": "Who is your mortgage with now?", "note": null },
          { "key": "plans", "prompt": "Any plans to move in the next five years?", "note": "Ask about a cottage too." }
        ]
      }
    ],
    "created_by_user_id": "cmuser0001",
    "created_at": "2026-10-09T15:00:00.000Z",
    "updated_at": "2026-10-09T15:00:00.000Z"
  }
}
```

## Errors

| Status | Code               | What it means                                                                                                    |
| ------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_ERROR` | Fix the fields in `details.fieldErrors`. A call needs a `contact_id`, a `deal_id`, or the client's number.       |
| `404`  | `NOT_FOUND`        | This key can't write to that `contact_id` or `deal_id`.                                                          |
| `409`  | `CALL_EXISTS`      | The call is already in the log of a broker whose calls this key can't read. Don't send it again.                 |

Every other code is on [Errors](/errors).
