# Authentication and keys

Every request carries an API key in the `Authorization` header, as a bearer token:

<CodeTabs syncKey="lang">

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

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

```python title="Python"
import os

import requests

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

</CodeTabs>

A missing, malformed, or revoked key, or a key whose broker was deactivated, all get the same
answer. The message is identical on purpose, so someone guessing keys learns nothing from it.

```json
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "Invalid API key" } }
```

## Create a key

An admin of the brokerage creates keys.

1. In BrokerPlus, go to **Settings > Developer**.
2. Select **New key**, name it after the integration that will use it, and pick any
   [limits](#limits-on-a-key).
3. Copy the key. BrokerPlus stores only a hash, so this is the one time you can see it.

A live key starts with `bp_live_`. A [sandbox](/sandbox) key starts with `bp_test_`. A brokerage
can have ten live keys at a time; revoke one to make an eleventh. If you lose a key, revoke it and
make a new one.

## What a key can do

There are no scopes. **A key acts as the broker who created it**, on the team they were working in
when they created it. It sees what that broker sees and can change what that broker can change,
and nothing more. If the broker's access shrinks, so does the key's. Keys made by a view-only
member can read, and every write returns `403 VIEW_ONLY`.

So protect a key the way you would protect that broker's password:

- Keep keys on a server. Never ship one in a browser bundle, a mobile app, or a public repository.
- Give each integration its own key, so you can revoke one without breaking the others.
- Revoke a key the moment you think it leaked. Revocation applies to the very next request.

`GET /v1/me` shows the brokerage, the broker, the team, and the limits of the key you are using.
Call it first when something does not behave the way you expect.

## Limits on a key

A limit takes access away from a key. No limit adds access, so a limited key never does more than
its broker. You choose limits when you create the key, and you cannot change them later: revoke the
key and make another.

| Limit          | What the key cannot do                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `read_only`    | Anything but `GET`.                                                                                                      |
| `leads_only`   | Anything outside the lead endpoints, `GET /v1/me`, and `GET /v1/custom-fields`. It also cannot convert a lead to a deal, or change the lead stages, lead channels, or referral partners. |
| `no_sending`   | Anything that puts something in front of a client: a message, a booking, a campaign, an intake email, an equity report, documents to sign. Without this limit, those requests go out at once. See [Reaching clients](/reaching-clients). |
| `no_documents` | Any document endpoint, and message attachments.                                                                          |

Pick the smallest set that works:

| Integration                                   | Limits         |
| --------------------------------------------- | -------------- |
| A website lead form                           | `leads_only`   |
| A report, a dashboard, or a warehouse sync    | `read_only`    |
| An assistant that drafts work for the broker  | `no_sending`   |
| Anything that must not contact clients        | `no_sending`   |
| Anything that must not see client files       | `no_documents` |

A request must pass every limit on the key. A refused request returns `403`, with the limit in
`details.limit`:

```json
{
  "success": false,
  "error": {
    "code": "KEY_LIMITED",
    "message": "This API key can only reach leads. Use a key without the leads-only limit for this request",
    "details": { "limit": "leads_only" }
  }
}
```

A `read_only` key gets `READ_ONLY_KEY` instead, without `details`. Limits also apply to
[webhooks](/webhooks): a key cannot subscribe to events it could not read.

## Connect an assistant without a key

Claude on the web, ChatGPT, and other apps that support MCP connectors don't need a key. They sign
in to BrokerPlus with OAuth 2.1, and the broker picks the team and whether the app can only read.
The connection then acts exactly like a key with the same team and read-only choice. It shows up
with the keys in **Settings > Developer**, and revoking it there disconnects the app.

See [Run an AI assistant](/mcp) for the steps.

## Rate limit

Each key can make **600 requests a minute**. Every response tells you where you stand:

| Header                | Meaning                                                       |
| --------------------- | ------------------------------------------------------------- |
| `RateLimit-Limit`     | Always `600`.                                                 |
| `RateLimit-Remaining` | Requests left in the current window.                          |
| `Retry-After`         | Seconds until the window resets. Sent only with a `429`.      |

Over the limit, the API returns `429 RATE_LIMITED`. Wait `Retry-After` seconds and send the request
again.

The window is fixed, not rolling. It starts at your first request and resets 60 seconds later. Each
item in a [batch write](/idempotency#batch-writes) counts as one request. If a sync keeps hitting the
limit, read only what changed: see [Staying in sync](/sync).
