# Templates

A template is the words of one message: a subject and a body for an email, or just a body for a
text. [Automations](/automations) send templates, and a template never sends anything on its own.
This page shows you how to write one, and how to use merge fields to fill in the client's name, the
property address, and the rest.

## What a key sees

The API reads and writes the same Pulse library brokers see in BrokerPlus. A key sees the templates
of every team its broker belongs to, and a new template goes to the team the broker is working in.
Creating, changing, or archiving one needs permission to change automations; without it the API
returns `403 AUTOMATION_MANAGE_FORBIDDEN`.

Two kinds of template aren't in the API: task templates (they describe a task an automation files,
not a message), and the follow-up copy of a named outreach variant, which is edited beside its
variant in BrokerPlus.

`attachments` lists the files sent with each email from a template, with their name, size, and type.
Add or remove attachments in BrokerPlus.

## Find the merge fields

A merge field drops a value from the file into the message, such as the client's first name.
`GET /v1/templates/merge-fields` lists every one, including your brokerage's custom fields.

<CodeTabs syncKey="lang">

```bash title="curl"
curl "https://api.mos.getbrokerplus.ca/v1/templates/merge-fields" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/templates/merge-fields", {
  headers: { Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}` },
});
const { data } = await res.json();
```

```python title="Python"
import os

import requests

res = requests.get(
    "https://api.mos.getbrokerplus.ca/v1/templates/merge-fields",
    headers={"Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}"},
)
data = res.json()["data"]
```

</CodeTabs>

```json
{
  "success": true,
  "data": [
    {
      "name": "client.firstName",
      "label": "Client first name",
      "section": "Client",
      "format": "text",
      "example": "Priya",
      "available_on": "any",
      "may_be_empty": false
    },
    {
      "name": "deal.financingConditionDate",
      "label": "Financing condition date",
      "section": "Deal",
      "format": "date",
      "example": "June 6, 2026",
      "available_on": "deal",
      "may_be_empty": true
    }
  ]
}
```

The list is long. These are the fields a stage-change email usually needs:

| Merge field | Value |
| --- | --- |
| `client.firstName`, `client.fullName` | The client the message goes to. |
| `partner.realtor.fullName`, `partner.realtor.email`, `partner.realtor.phone` | The Realtor on the deal. |
| `property.fullAddress` | The address of the subject property. |
| `deal.financingConditionDate` | The financing condition date. |
| `broker.fullName`, `broker.email`, `broker.phone`, `broker.signature` | The broker the message goes out as. |

`available_on` tells you which automations can fill a field. A `deal` field is empty in an
automation that runs on a contact with no deal, so keep `deal` fields out of contact automations.

## Write a merge field

Write a merge field between double braces: `{{client.firstName}}`. A bare merge field is required:
if the file has no value for it, BrokerPlus holds the message for the broker rather than send it with
a gap.

When the value can be missing, add a fallback after a vertical bar:
`{{partner.realtor.firstName|your Realtor}}`. Give every field whose `may_be_empty` is `true` a
fallback.

A merge field that isn't in the list is refused with `400 UNKNOWN_MERGE_FIELDS`, and
`error.details.unknown` names each one.

## Create a template

`POST /v1/templates` creates a template. An email template needs a `subject`. A text template has
neither `subject` nor `body_html`, and sending either one is refused.

<CodeTabs syncKey="lang">

```bash title="curl"
curl -X POST "https://api.mos.getbrokerplus.ca/v1/templates" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Conditions waived",
    "channel": "email",
    "subject": "Your financing is firm, {{client.firstName}}",
    "body": "Hi {{client.firstName}},\n\nThe financing conditions on {{property.fullAddress}} are waived. {{partner.realtor.firstName|Your Realtor}} knows.\n\n{{broker.fullName}}"
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/templates", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Conditions waived",
    channel: "email",
    subject: "Your financing is firm, {{client.firstName}}",
    body: "Hi {{client.firstName}},\n\nThe financing conditions on {{property.fullAddress}} are waived. {{partner.realtor.firstName|Your Realtor}} knows.\n\n{{broker.fullName}}",
  }),
});
const { data } = await res.json();
```

```python title="Python"
import os

import requests

res = requests.post(
    "https://api.mos.getbrokerplus.ca/v1/templates",
    headers={"Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}"},
    json={
        "name": "Conditions waived",
        "channel": "email",
        "subject": "Your financing is firm, {{client.firstName}}",
        "body": "Hi {{client.firstName}},\n\nThe financing conditions on {{property.fullAddress}} are waived. {{partner.realtor.firstName|Your Realtor}} knows.\n\n{{broker.fullName}}",
    },
)
data = res.json()["data"]
```

</CodeTabs>

```json
{
  "success": true,
  "data": {
    "object": "template",
    "id": "cmt8k2v0q00",
    "name": "Conditions waived",
    "channel": "email",
    "subject": "Your financing is firm, {{client.firstName}}",
    "body": "Hi {{client.firstName}},\n\nThe financing conditions on {{property.fullAddress}} are waived. {{partner.realtor.firstName|Your Realtor}} knows.\n\n{{broker.fullName}}",
    "body_html": null,
    "merge_fields": [
      { "name": "client.firstName", "fallback": null },
      { "name": "property.fullAddress", "fallback": null },
      { "name": "partner.realtor.firstName", "fallback": "Your Realtor" },
      { "name": "broker.fullName", "fallback": null }
    ],
    "attachments": [],
    "team_id": "cmt1q9x4a00",
    "archived_at": null,
    "created_at": "2026-10-07T15:02:11.481Z",
    "updated_at": "2026-10-07T15:02:11.481Z"
  }
}
```

To send it, name the template's `id` in an `email` or `sms` step of an
[automation](/automations#example-email-the-client-and-the-realtor-when-a-deal-moves-into-a-stage).

## Change a template

`PATCH /v1/templates/{id}` changes the fields you send. Every automation that sends the template uses
the new words from its next message on.

`body_html` is the formatted version of an email. If you send a new `body` without a new `body_html`,
BrokerPlus drops the formatted version, because it still holds the old words. Send both to keep a
formatted email. Scripts and unsafe markup are stripped from `body_html` before it is saved.

A template's `channel` can't change. Create a new template instead.

## Archive a template

`DELETE /v1/templates/{id}` archives the template. Messages it already sent stay on the file. If an
automation still sends it, the API returns `409 TEMPLATE_IN_USE`: take it out of that automation
first, with `PATCH /v1/automations/{id}` or in BrokerPlus.

`GET /v1/templates` leaves archived templates out. Add `include_archived=true` to see them.

## Outreach templates

Outreach templates are the first message a broker sends by hand from the refinance, renewal, and
lead lanes, as the outreach templates editor shows them. They are separate from the Pulse library
above: automations never send them. `kind` names the lane and the channel: `refinance_initial`,
`renewal_initial`, and `leads_initial` are emails, and `refinance_sms`, `renewal_sms`, and
`leads_sms` are texts.

Each broker keeps their own set, and their team keeps a shared set. `GET /v1/outreach-templates`
lists both; `scope` says which set a template is in. A teammate's own templates are never listed.
The first time a lane is listed, the broker's own set gets the default copy, as in the editor.

- `POST /v1/outreach-templates` adds one to the broker's own set, or to the team's with
  `"scope": "team"`. Send no `subject` and no `body` to start from the lane's default copy.
- `PATCH` and `DELETE /v1/outreach-templates/{id}` change or remove one. The broker's own set always
  keeps one template per lane.
- `body_html` is the formatted version of an email template, as the broker's editor saves it. Send
  it on `PATCH` with `body` as the plain-text version of the same message. A new `body` without a new
  `body_html` removes the formatted version, and null goes back to plain text. Scripts and unsafe
  markup are stripped before it is saved, and a text template never keeps one.
- `POST /v1/outreach-templates/{id}/set-primary` makes it the one the lane starts from when the
  broker sends. Each set has one primary per lane.
- `POST /v1/outreach-templates/{id}/duplicate` copies it in its own set, `/share` copies one of the
  broker's own into the team's set, and `/copy-to-personal` copies one of the team's into the
  broker's own.

Only a broker who can send email can write the team's set; anyone else gets `403`. Nothing here
sends a message.

## Media library

The media library holds the pictures and video stills the brokerage puts in outreach emails. Every
broker in the brokerage shares one library.

- `GET /v1/media-assets` lists it, newest first. `url` is the picture's permanent public address,
  the one an email shows.
- `POST /v1/media-assets` uploads a picture: PNG, JPEG, WebP, or GIF, at most 10 MB and 2000 pixels
  on its longest side. Send JSON with `file_name` and `content_base64`, or `multipart/form-data`
  with the file in a field named `file`.
- `POST /v1/media-assets/video-links` adds a YouTube, Vimeo, Loom, NeetoRecord, Instagram, or
  Facebook video as a still with a play button, linking to the video.
- `DELETE /v1/media-assets/{id}` removes one. An email already sent that shows it shows a broken
  picture from then on.

```bash
curl -X POST "https://api.mos.getbrokerplus.ca/v1/media-assets/video-links" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "name": "Renewal walkthrough"}'
```
