# RLP CRM API reference

Routes the CRM web app calls, taken from its JavaScript bundle on 2026-11-04. Paths are
relative to `/api/data/`, so `rlpCrm.get('accounts/:id', {id: 5})` calls
`GET /api/data/accounts/5`. Path parameters (`:id`) come from the params object; for GET and
DELETE the other params become the query string, for POST/PUT/PATCH they become the JSON body.

## Contents
- Response shape and quirks
- Who am I, teams, users
- Accounts
- Contacts
- Notes (activity, news)
- Tasks
- Inbox, kanban, prompts, news, schema
- AI chat sessions

## Response shape and quirks

- Single-record routes (`accounts/:id`, `contacts/:id`, `notes/:id`, `tasks/:id`) return one
  object; list routes (including `who`, `.../notes`, `.../contacts`) return arrays.
- `accounts/search` and `contacts/search` return mixed hits:
  `{id, name, accountName, type: 'Account', accountId, matchCount}` or
  `{id, name, accountName, type: 'Contact', contactId, personId, matchCount}`. `id` equals the
  `accountId` or `contactId` of the hit.
- Records keep most fields in a JSON string column `data`. `rlpCrm` merges it into the row and
  camelCases keys, the same way the app does. Field names inside `data` differ between records,
  so look at `Object.keys(row)` for one record before filtering on a field.
- `accounts`, `contacts`, `team/:teamId/...` and `users/:userId/...` return every row the user can
  see. There is no paging and `limit` is ignored, so filter and map in the page.
- `accounts/count` and `contacts/count` return one row per team (`{count, teamId}`) plus one
  without `teamId` for records not on a team.
- Access level is 1–4. What a user sees depends on their teams and level.
- `users/me/pat` (personal access token) returned HTTP 500 `Invalid column name 'Pat'` on
  2026-11-04, so there is no long-lived token for scripts outside the browser.

## Who am I, teams, users

| Method | Route | Params |
|---|---|---|
| GET | `who` | — returns one row per team: `{userId, teamId, teamName, accessLevel}` |
| GET | `teams` | — `{teamId, teamName}` |
| GET | `teams/:id`, `teams/:id/users`, `teams/users` | `id` |
| POST | `teams/:id/users` | `id`, `userId` — add user to team |
| DELETE | `teams/:id/users/:userId` | remove user from team |
| DELETE | `teams/:id` | delete team |
| GET | `users` | — all users |
| PUT | `users/:userId/access-level` | `accessLevel` 1–4 |
| GET/DELETE | `users/me/pat` | personal access token (broken, see above) |

## Accounts

| Method | Route | Params |
|---|---|---|
| GET | `accounts` | — all visible accounts: RLP brokerages (have `rlpCompanyId`) and prospects/competitors (have `teamId`, `brand`); see SKILL.md "What the accounts are" |
| GET | `accounts/count` | — |
| GET | `accounts/search` | `q` |
| GET | `accounts/:id` | `id` |
| GET | `accounts/:id/contacts` | `id`, optional `initialContactId` |
| GET | `accounts/:id/notes`, `accounts/:id/tasks` | `id` |
| GET | `team/:teamId/accounts` | `teamId` |
| GET | `users/:userId/accounts` | `userId` — accounts the user follows |
| POST | `accounts` | `name`, `data` (JSON string), `teamId` |
| PATCH | `accounts/:id` | `name`, optional `data` (JSON string — send the whole object) |
| DELETE | `accounts/:id` | `id` |
| POST / DELETE | `users/:userId/accounts` / `users/:userId/accounts/:id` | follow / unfollow |

## Contacts

| Method | Route | Params |
|---|---|---|
| GET | `contacts` | — `id, accountId, displayName, rlpPersonId, rlpCompanyId, accountName` + data fields |
| GET | `contacts/count` | — |
| GET | `contacts/search` | `q` — visible contacts |
| GET | `contacts/searchAll` | `q` — wider search |
| GET | `contacts/:id`, `contacts/:id/notes`, `contacts/:id/tasks` | `id` |
| GET | `team/:teamId/contacts` | `teamId` |
| GET | `users/:userId/contacts` | `userId` |
| POST | `contacts` | `name`, `data` (JSON string), `accountId` |
| PUT | `contacts` | `personId` — add an RLP person as a contact |
| PATCH | `contacts/:id` | optional `data` (JSON string) |
| DELETE | `contacts/:id` | `id` |
| POST / DELETE | `users/:userId/contacts` / `users/:userId/contacts/:id` | follow / unfollow |

## Notes (activity, news)

| Method | Route | Params |
|---|---|---|
| GET | `notes/count`, `users/me/notes`, `users/me/notes/count` | — (`users/me/notes/count` also takes the feed filters) |
| GET | `users/me/notes/page` | feed filters + `limit` (1–40, default 40); page with `beforeUpdatedAt`, `beforeId`, `beforeAssociationType`, `beforeAssociationId` from the last row |
| GET | `users/me/notes/calendar` | feed filters + `month` (`YYYY-MM-01`) |
| GET | `notes/:id`, `notes/:noteId/tasks` | `id` / `noteId` |
| GET | `notes/visibility-policy` | `objectType` (`account`/`contact`), `objectIds` (comma list) |
| POST | `notes` | `title`, `content`, `type`, `teamIds` (comma list or null), `objectIds` (comma list), `objectType`, `flags` (default 0), `visibilityMode` (`teams`/`levels`), `minimumLevel` (1–4, only with `levels`) |
| PUT | `notes/:id` | same body as POST without `objectIds`/`objectType` |
| PATCH | `notes/:id` | `startDate`, `dueDate` |
| PATCH | `notes/:id/status` | `status` (int) |
| DELETE | `notes/:id` | `id` |

Feed filters: `search` (≤200 chars), `teamId`, `province` (AB BC MB NB NL NS NT NU ON PE QC SK YT
east west), `minimumLevel` 1–4, `date` (`YYYY-MM-DD`), `newsOnly` (bool).

## Tasks

Status: `Open`, `Waiting`, `Done`, `Canceled`. Priority: `High`, `Medium`, `Low`.
Source type: `Manual`, `Note`.

| Method | Route | Params |
|---|---|---|
| GET | `tasks` | optional `accountId`, `contactId`, `noteId`, `assigneeId`, `status`, `includeDeleted`, `deletedOnly` |
| GET | `tasks/:id` | `id` |
| POST | `tasks` | `title` (1–255), `description`, `accountId`, `contactId`, `sourceNoteId`, `dueDate`, `status` (default Open), `priority` (default Medium), `sourceType` |
| PATCH | `tasks/:id` | same fields as POST |
| PATCH | `tasks/:id/status` | `status` |
| DELETE | `tasks/:id` | `id` |

## Inbox, kanban, prompts, news, schema

| Method | Route | Params |
|---|---|---|
| GET | `inbox` | `teamId`, `status`, `accountId`, `contactId`, `actionCategory`, `actionKey`, `q`, `signals`, `limit` (1–100), `view` |
| POST / PATCH / DELETE | `inbox`, `inbox/:inboxId` | inbox items |
| GET | `kanban`, `kanban/:id`, `kanban/schema/:schemaId` | boards |
| POST / PATCH / DELETE | `kanban`, `kanban/:id` | `schemaId`, `name` (1–100), `config` (JSON string) |
| GET | `prompts` | `category`, `favoritesOnly`, `searchTerm` |
| GET / PUT / DELETE | `prompts/:id` | saved AI prompts |
| POST | `prompts`, `prompts/:id/use` | `promptContent` |
| GET / POST | `news` | POST: `content`, `entityType`, `entityId`, `url`, `user` |
| GET / PATCH | `schema` | PATCH: `name`, `schema` — CRM field schema, admin only |

## AI chat sessions

The app's built-in assistant: `chat/sessions`, `chat/sessions/active`,
`chat/sessions/active/ensure`, `chat/sessions/:id`, `chat/sessions/:chatSessionId/messages`
and `/api/chat` (outside `/api/data`). These hold the user's own assistant history; leave them
alone unless the user asks about them.
