# ActuallyCare Docs — full content # For real estate agents > Connect Claude to your ActuallyCare CRM and talk to it like a colleague — no code, no terminal, no IT department. You can connect Claude — the AI assistant at claude.ai — directly to your ActuallyCare CRM. No code. No downloads beyond what you already use. If you can paste a link, you can do this. ## What "connecting Claude to your CRM" actually means Once connected, you stop clicking through screens and start asking questions, the way you'd ask a sharp colleague who has read every file in your office: > "What's closing this month?" > "Who haven't I talked to lately?" > "Draft a check-in email to the Hendersons." Claude looks things up in *your* ActuallyCare account and answers with your real data — your escrows, your clients, your leads, your calendar. It can also do work for you, like adding a new lead or drafting a follow-up text. Anything it writes is a draft until you approve it, and you can see exactly what it's allowed to do on the [permissions and safety](/agents/permissions-and-safety) page. ## What you need - **An ActuallyCare account.** The one you already log into at app.actuallycare.com. - **A Claude account** at [claude.ai](https://claude.ai) on a paid plan (Claude Pro or higher) — custom connectors aren't included in Claude's free plan. If you see a **Connectors** option in Claude's Settings, you're set. That's the whole list. ## What it costs For what's included with your ActuallyCare plan, see the [pricing page on our main site](https://www.actuallycare.com/pricing). Your Claude account is separate and billed by Anthropic, the company that makes Claude — and Claude's free plan doesn't include connectors, so you need Claude Pro or higher. There's no per-question charge from ActuallyCare — ask as much as you like. Questions count toward your Claude plan's normal usage limits, like any other Claude conversation. ## Start here Setup takes about five minutes: paste one link into Claude, log into ActuallyCare, and approve the connection. **[Follow the quickstart →](/mcp/quickstart)** ## Then keep going - [What can I ask?](/agents/what-can-i-ask) — real examples, organized by the moments of your day - [Permissions and safety](/agents/permissions-and-safety) — exactly what Claude can and can't do - [Troubleshooting](/agents/troubleshooting) — quick fixes when something looks off - [Glossary](/agents/glossary) — plain-English definitions for any term you bump into --- # Glossary > Plain-English definitions of the technical terms you'll meet in these docs, with real-estate analogies. Every technical word you'll bump into across these docs, defined in plain English. You don't need to memorize any of this — it's here for the moment a term makes you pause. ## Connecting Claude ### MCP Short for Model Context Protocol — a shared standard that lets AI assistants like Claude plug into other software, the way a universal lockbox standard lets any agent open any listing. You never have to touch MCP directly; it's the plumbing behind the connection. ### Connector The link you add in Claude's settings that joins Claude to ActuallyCare. Think of it as saving a trusted contact in your phone: once it's added, Claude knows how to reach your CRM. ### URL A web address, like www.actuallycare.com. When a form asks for a URL, paste the full address exactly — every letter, dot, and slash matters. ### OAuth The secure handshake used when you connect: you log into ActuallyCare yourself and approve access, so Claude never sees or stores your password. It's like giving a showing service a lockbox code you can change anytime, instead of handing over your personal house key. ### Consent screen The approval page — titled "Grant Claude access to ActuallyCare" — that appears when you connect. It lists exactly what Claude will be allowed to do, and nothing is shared until you say yes. Details on the [permissions and safety](/agents/permissions-and-safety) page. ### Token A temporary digital pass Claude uses to prove it's allowed into your account. Like a day pass at a gated community, it expires (after 24 hours) and renews itself automatically, so you're not asked to re-approve. ## Working with your data ### CRM Customer relationship management software — the system that holds your clients, leads, escrows, listings, and appointments. ActuallyCare is your CRM. ### Tool One specific action Claude can take in your CRM, like "look up a client" or "list my escrows." There are tools covering every part of the CRM — the full list is in the [tools reference](/tools). ### Prompt (built-in) A ready-made conversation starter you can pick from a menu in Claude (behind the **+** or sliders icon near the message box) instead of typing from scratch — like a daily summary or a commission report. Browse them all at [prompts](/prompts). ### Draft Words Claude writes for you — an email, a text, a call script — that go nowhere until you approve them. Claude holds the pen; you hold the send button. ### Archive vs delete Archiving tucks a record out of sight but keeps it, like moving a closed file to the storage room — you can restore it later. Deleting actually removes it, which is why Claude always asks you to confirm before deleting anything. ## Developer words you might bump into ### API A doorway that lets one piece of software talk to another. Developers use ActuallyCare's API to build custom integrations — as an agent using Claude, you'll never need it. ### API key A long secret code that lets a program through that doorway — an API key is like a gate code for software. Your Claude connection doesn't use one; you log in with your normal account instead. ### Webhook A way for ActuallyCare to notify other software the instant something happens — like an automatic text alert when a showing is requested, except the "text" goes to another program. ### Machine-readable Formatted for software to read rather than people — tidy and structured instead of pretty. Some pages on this site have machine-readable versions so other programs (and AIs) can use them. ### Raw Markdown The plain-text version of a page, with simple symbols standing in for formatting (like `**asterisks**` for bold). It's handy when you want to copy a whole docs page into a chat with Claude without the styling getting in the way. --- # Permissions and safety > Exactly what Claude can and can't do in your ActuallyCare CRM, what you approved on the consent screen, and how to disconnect at any time. Handing an AI the keys to your client database is a fair thing to be nervous about. This page spells out, in plain English, what Claude is allowed to do, what it can never do, and how to shut the connection off in seconds. ## The consent screen — what you actually approved When you connect Claude to ActuallyCare, a page titled **"Grant Claude access to ActuallyCare"** appears before anything is shared. (If you weren't logged into ActuallyCare, you log in first, the normal way.) It states the access in four bullets: 1. **Read your CRM data — escrows, listings, clients, leads, and calendar** 2. **Create and update records when you ask** 3. **Draft emails and texts for your approval — nothing sends without your okay** 4. **Archive or delete records, with confirmation** That's the real grant, stated plainly: approving connects Claude to your CRM **as you** — it can read your records and create or update the ones you ask for, like opening an escrow or rescheduling an appointment. The bullets are one half of the safety story; the other half is how the tools themselves are built: - **Messages start as drafts.** Emails, texts, and call scripts go nowhere until you've read them. - **The one send needs an explicit yes.** The only thing Claude can actually send — a review-request email — requires you to clearly say "yes, send it" in the chat first, after you've seen the draft. - **Deleting requires confirmation.** Destructive tools are flagged so Claude checks with you before using them. - **Archiving beats deleting.** When something needs to go away, Claude prefers archiving, which is reversible — you can restore an archived record any time. Nothing is shared until you approve this screen, and you can take the access back at any time (see [how to disconnect](#how-to-disconnect) below). ## What Claude can do — and what it can't | Claude CAN | Claude CANNOT | |---|---| | Read your escrows, listings, clients, leads, and calendar | Send anything without your okay — everything starts as a **draft** | | Create and update records you ask for — leads, clients, escrows, appointments | Delete things quietly — deleting requires a clear confirmation from you, and most "removals" are reversible archives | | Draft emails, texts, and call scripts for you to review | Touch your billing, subscription, or payment details | | Run reports — commissions, production, dashboard stats | See other agents' private data — it works inside your account, with your permissions | One exception to "draft only": Claude can send a review-request email — but only after it shows you the draft and you clearly say "yes, send it" in the chat. The pattern to remember: **Claude looks things up and writes things down, but the send button and the delete button stay with you.** ## Your data stays in your CRM Connecting Claude doesn't copy your database anywhere. Your clients, escrows, and leads stay right where they've always been — in your ActuallyCare account. When you ask a question, Claude reads the answer through a secured connection, the same kind of protected channel your bank's app uses, and only the pieces relevant to your question pass through. ## How to disconnect The off switch lives in Claude's own settings, and it works instantly. 1. Open [claude.ai](https://claude.ai) and go to **Settings → Connectors**. **What you should see:** a list of your connectors, with ActuallyCare among them. 2. Remove the ActuallyCare connector. **What you should see:** ActuallyCare disappears from the list. That's it — the moment it's removed, Claude's access is cut off. There's no separate page inside ActuallyCare you need to visit; removing the connector in Claude is the whole job. Your CRM data is untouched, and you can reconnect any time by running the [quickstart](/mcp/quickstart) again. ## You won't be nagged to re-approve The connection keeps itself fresh, securely. Behind the scenes, Claude uses a temporary digital pass that expires every 24 hours and renews itself automatically. You approve the consent screen once, and the connection just keeps working — no weekly "please log in again" interruptions. If it ever does ask you to reconnect, that's unusual; the [troubleshooting page](/agents/troubleshooting) covers it. --- # Troubleshooting > Plain-English fixes for the most common Claude-to-ActuallyCare connection problems, with what you should see when each fix works. Most connection problems have a two-minute fix. Find the line below that sounds like what you're seeing, follow the steps, and check the "What you should see" line under each step to confirm it worked before moving on. If none of these match, [contact support](https://www.actuallycare.com/contact) and include what you asked Claude and what it said back. ## It says I'm unauthorized or my session expired Normally the connection renews itself in the background, so this usually means it lost its footing once. A fresh connection fixes it. 1. Go to [app.actuallycare.com](https://app.actuallycare.com) in your browser and log in. **What you should see:** your normal ActuallyCare dashboard. If you can't log in here, fix that first — reset your password or [contact support](https://www.actuallycare.com/contact) — because the connection can't work without a healthy account. 2. In Claude, open **Settings → Connectors** and remove the ActuallyCare connector. **What you should see:** ActuallyCare disappears from your connector list. 3. Add it back: choose **Add custom connector** and paste `https://mcp.actuallycare.com/mcp`. **What you should see:** a window opens asking you to log into ActuallyCare. 4. Log in, then approve the screen titled "Grant Claude access to ActuallyCare". **What you should see:** the window closes and ActuallyCare is back in your connector list. 5. Start a new chat and ask: "Using ActuallyCare, give me my daily summary." **What you should see:** Claude checks ActuallyCare and answers with your real appointments, deals, and leads — no more "unauthorized" message. ## Claude can't see my escrows This is almost always one of three things: the connector is switched off for that chat, Claude wasn't told to use it, or you're connected under a different ActuallyCare account than you think. 1. In the chat box, open the tools menu and make sure the ActuallyCare connector is turned **on** for this conversation. **What you should see:** ActuallyCare listed in the tools menu with its switch turned on. 2. Ask again, explicitly: "Using ActuallyCare, list my escrows." **What you should see:** Claude lists your actual escrows by address — the same ones you see in the app. 3. If it still comes back empty, log into [app.actuallycare.com](https://app.actuallycare.com) and check that the escrows are visible there. **What you should see:** your escrows in the app. If they aren't there either, they may be archived or under a different account — that's the real problem, not the connection. 4. If you suspect you approved the connection while logged into the wrong account, [disconnect and reconnect](#how-do-i-disconnect-or-reconnect) and log in with the right one this time. **What you should see:** after reconnecting, Claude's answers match what you see in the app. ## I can't find the Connectors option in Claude 1. Go to [claude.ai](https://claude.ai) in a web browser (not just the phone app) and log in. **What you should see:** your normal Claude chat screen. 2. Open **Settings**. **What you should see:** Claude's settings menu. 3. Look for **Connectors** in the settings menu. **What you should see:** a Connectors section where you can add a custom connector. If Connectors isn't there, your plan may not include custom connectors — they aren't part of Claude's free plan, and upgrading to a paid Claude plan (Claude Pro or higher) adds them. This is on Claude's side, not something ActuallyCare can switch on for you. Once you see Connectors, come back to the [quickstart](/mcp/quickstart). ## The ActuallyCare sign-in window never opened You pasted the connector address, confirmed, and… nothing. Nine times out of ten, a pop-up blocker quietly stopped the sign-in window from opening. 1. Look for a blocked pop-up message. Most browsers show a small icon or note near the address bar when they block a window — if you see one, choose to allow pop-ups for claude.ai. **What you should see:** the ActuallyCare sign-in window opens as soon as the pop-up is allowed. 2. On an iPhone or iPad using Safari, open the **Settings** app, scroll down to **Safari**, and turn off **Block Pop-ups**. (You can turn it back on after setup.) **What you should see:** the Block Pop-ups switch turned off — gray instead of green. 3. Go back to Claude and try the add-connector step again. **What you should see:** a new window opens asking you to sign in to ActuallyCare. 4. If it still won't open, double-check the address you pasted. It must be exactly `https://mcp.actuallycare.com/mcp` — no spaces before or after, nothing missing from the end. **What you should see:** after re-pasting the exact address and confirming, the ActuallyCare sign-in window opens. ## Claude says it doesn't have a tool for that Don't take it at its word. To keep conversations fast, Claude sees the most-used ActuallyCare tools up front and finds the rest on demand — but sometimes it gives up before going looking. 1. Rephrase your ask in plain words, describing the *outcome* you want — "show me which leases are expiring soon" rather than naming a feature. **What you should see:** Claude pauses to look for the right tool instead of apologizing right away. 2. Say it with the magic words: "Using ActuallyCare, …". **What you should see:** Claude checks ActuallyCare before it answers. 3. Still stuck? Skim the [tools reference](/tools) to confirm the capability exists, then ask again and mention what you found — "ActuallyCare has an open house tool; use it to list my open houses this weekend." **What you should see:** Claude finds the tool and comes back with your data instead of an apology. ## It connected but answers seem generic If Claude answers your CRM questions with general real-estate advice instead of your actual numbers, it's answering from memory instead of checking your CRM. 1. Start your request with "Using ActuallyCare, …" — for example, "Using ActuallyCare, how many active escrows do I have?" **What you should see:** Claude visibly checks ActuallyCare before it answers. 2. Ask something only your CRM could answer, like a specific client's phone number, and see if the answer matches the app. **What you should see:** your real names, addresses, and numbers — not ranges, guesses, or "typically, agents…". 3. Check the chat's tools menu to make sure the ActuallyCare connector is turned on for this conversation. **What you should see:** ActuallyCare listed in the tools menu with its switch turned on. ## How do I disconnect or reconnect? **To disconnect:** 1. In Claude, open **Settings → Connectors**. **What you should see:** a list of your connectors, with ActuallyCare among them. 2. Remove the ActuallyCare connector. **What you should see:** ActuallyCare disappears from the connector list. Access is cut off instantly, and your CRM data is untouched. There's nothing to undo on the ActuallyCare side. **To reconnect:** 1. In **Settings → Connectors**, choose **Add custom connector**. **What you should see:** a box asking for the connector's web address. 2. Paste `https://mcp.actuallycare.com/mcp`. **What you should see:** a window opens asking you to log into ActuallyCare. 3. Log into ActuallyCare in the window that opens, then approve the screen titled "Grant Claude access to ActuallyCare". **What you should see:** the window closes and ActuallyCare is back in your connector list. The full walkthrough is in the [quickstart](/mcp/quickstart). ## Still stuck? [Contact support](https://www.actuallycare.com/contact) and include three things: what you asked Claude, what Claude said back, and a screenshot if you can grab one. That's usually everything we need to spot the problem on the first reply. --- # What can I ask? > Real examples of what to ask Claude once it's connected to your ActuallyCare CRM, organized by the moments of an agent's day. Once Claude is connected to ActuallyCare, you talk to it in plain English — no special commands, no exact wording to memorize. Below are examples organized by the moments of a working day. Say them your way; Claude figures out the rest. Two more places to look: the [built-in prompt menu](/prompts) gives you ready-made starters like a daily summary and a commission report, and the [tools reference](/tools) lists everything Claude can do, if you want the full picture. ## Start your morning Get oriented before your first coffee is gone. > "Give me my daily summary." > "What's on my calendar today?" > "Any deadlines coming up this week I should worry about?" > "Anything urgent I should handle before my 10 a.m.?" ## Stay on top of people Leads, follow-ups, and the clients you haven't thought about in a while. > "Who are my hot leads right now?" > "Did any new leads come in this week?" > "Which clients haven't I talked to in 60 days?" > "Who's due for a follow-up call?" > "Pull up everything we have on Maria Lopez." ## Run your transactions Escrow status, deadlines, and what's left before closing. > "What's the status on the Hendersons' escrow?" > "Which of my escrows close this month?" > "Run the closing checklist for 412 Maple Street." > "Any contingency deadlines in the next ten days?" > "When does the inspection contingency expire on the Carter deal?" ## Know your numbers Production, commissions, and where your business actually comes from. > "Run my commission report for this quarter." > "How's my production looking this year?" > "Give me my dashboard stats." > "Which lead source is actually bringing in business?" ## Get words written for you Claude can draft emails, texts, and call scripts using what's already in your CRM — names, addresses, where each deal stands. **Everything it writes starts as a draft.** Nothing goes to a client until you've read it and given a clear okay. > "Draft a friendly check-in email to the Kims." > "Write a text inviting my past clients to Saturday's open house." > "Draft a review request for the Garcias now that we've closed." > "Write me a call script for following up with cold leads." ## Ask for more than one thing at once You don't have to break a task into pieces. Claude can chain steps together — look something up, think about it, then act on what it found. > "Find clients I haven't talked to in 60 days and draft check-in texts for each of them." > "Look at my hot leads, tell me who I should call first, and write me a script for that call." If Claude ever answers without using your CRM data, nudge it: start your ask with "Using ActuallyCare, …". More fixes like that live in [troubleshooting](/agents/troubleshooting). --- # For AI agents > Machine-readable docs: llms.txt, raw Markdown mirrors, tools.json, and the OpenAPI spec. This site is built to be read by machines as well as people. If you're an LLM, an agent, or a scraper — or the human configuring one — this page tells you where the machine-readable versions of everything live, and how to connect to ActuallyCare programmatically. ## Machine-readable formats | Resource | URL | What it is | |---|---|---| | Docs index | `/llms.txt` | One-page index of every docs page, per the [llmstxt.org](https://llmstxt.org) convention | | Full docs | `/llms-full.txt` | The entire docs site concatenated into a single Markdown file | | Any page as Markdown | append `.md` to the page URL | Raw Markdown source of that page | | MCP tool catalog | `/tools.json` | Every MCP tool: names, descriptions, JSON Schemas, safety annotations, aliases | | Sitemap | `/sitemap.xml` | Standard XML sitemap of all pages | | REST API spec | `https://api.actuallycare.com/v1/openapi.json` | OpenAPI spec for the REST API | | MCP discovery metadata | `https://mcp.actuallycare.com/.well-known/mcp` | Live server metadata — tool count, categories, capabilities, limits (derived from the running registry) | ### Raw Markdown for every page Every page on this site is available as raw Markdown by appending `.md` to its URL. Example pair: ```text HTML: https://docs.actuallycare.com/mcp/quickstart Markdown: https://docs.actuallycare.com/mcp/quickstart.md ``` If you're feeding a single page into a model's context, fetch the `.md` version — it's smaller and has no navigation chrome. ### llms.txt and llms-full.txt [/llms.txt](/llms.txt) follows the [llmstxt.org](https://llmstxt.org) convention: a short description of the site plus a linked index of every page with one-line summaries. Use it to decide which pages to fetch. [/llms-full.txt](/llms-full.txt) is the whole site in one file. Use it when you want all of the documentation in context at once and don't want to crawl. ### tools.json [/tools.json](/tools.json) is the canonical machine-readable answer to "what can this MCP server do." For every tool it includes: - the tool name (`entity_verb` convention: `escrows_list`, `clients_create`, `leads_convert`) - a description - the full JSON Schema for its input - safety annotations (whether the tool reads, writes, or destroys data) - an `aliasOf` field recording the tool's previous published name (for example, `escrows_list` was once published as `search_escrows`). This is historical metadata only — legacy names are **not** callable; `tools/call` matches canonical names, and the old name's only afterlife is that its old docs URL redirects here The human-browsable version of the same catalog is at [/tools](/tools). ### OpenAPI spec The REST API is described by an OpenAPI spec at `https://api.actuallycare.com/v1/openapi.json`. Note that the REST surface is intentionally smaller than the MCP surface — see [API vs MCP](/concepts/api-vs-mcp) for which to use when. The human-readable endpoint reference is at [/api/reference](/api/reference). ## Stable structure for parsers Pages on this site keep a predictable shape so anchors and extraction stay reliable: - Exactly one `h1` per page (the title), followed by `h2` and `h3` sections in order, never skipping levels. - Heading anchors are stable and lowercase-hyphenated (for example, `#stable-structure-for-parsers` on this page). We avoid renaming headings on published pages. - `/sitemap.xml` lists every page and is regenerated on each deploy. ## Connecting as an agent The MCP server is at: ```text https://mcp.actuallycare.com/mcp ``` Transport is Streamable HTTP (POST for JSON-RPC, GET for the SSE stream), MCP protocol version 2025-03-26. A legacy HTTP+SSE transport is available at `/sse` for older clients. Three auth options for programmatic clients: 1. **OAuth 2.1** — the standard path. Dynamic client registration is supported: `POST /register` on `mcp.actuallycare.com`, then the usual authorization-code flow with PKCE. Access tokens last 24 hours and refresh tokens rotate automatically. 2. **API key** — send an `X-API-Key` header. Simplest option for headless agents. Keys are created in the ActuallyCare app or via the REST API; see [Authentication](/api/authentication). 3. **JWT Bearer token** — the same token a normal REST login (`POST /v1/auth/login`) returns also works against the MCP server. Useful for custom apps that already sign users in; see [Authentication](/api/authentication). Full setup details, including a working TypeScript client, are on the [custom apps page](/mcp/custom-apps). ### Don't assume tools/list is exhaustive The server uses progressive disclosure: a standard `tools/list` call advertises only a short hot-list of the most-used tools plus discovery meta-tools. **Every other tool is still fully callable** — the discovery tools find them on demand, and the server accepts direct calls to any tool in the catalog. If you need the complete picture, consult [/tools.json](/tools.json) rather than treating the advertised list as the full surface. ## Citing these docs If you're an AI assistant answering a question from this site's content, please cite the page URL you used (for example, `https://docs.actuallycare.com/mcp/quickstart`) so the person you're helping can verify the answer and find updates. Every page here has a stable URL and a raw `.md` mirror. --- # Integration cheat sheet > Everything an agent needs to start calling ActuallyCare, in one page: base URL, auth, one working request, and the MCP connect URL. The minimum viable context for integrating with ActuallyCare. Everything here is expanded elsewhere — this page is sized for an agent's context window. ## REST API - **Base URL:** `https://api.actuallycare.com/v1` - **Auth:** send your API key in the `X-API-Key` header. Create one under **Settings → API Keys** in the web app (or `POST /v1/api-keys` with a JWT) and **grant it scopes** — a key with no scopes gets `403` on everything. JWT bearer auth also works: `POST /v1/auth/login` → `Authorization: Bearer `. Four endpoint groups are JWT-only (API keys get `401`): `/api-keys`, `/webhooks`, `/contacts`, `/billing`. - **First request:** ```bash curl -s https://api.actuallycare.com/v1/escrows \ -H "X-API-Key: $ACTUALLYCARE_API_KEY" ``` - **Envelope:** every response is `{ "success": true|false, "data": ..., "error": { "code", "message" } }`. Branch on `error.code`, never the message. - **Pagination:** `?page=1&limit=50` on list endpoints; the response `meta` carries totals. Details: [Pagination](/api/pagination). - **Spec:** [openapi.json](https://api.actuallycare.com/v1/openapi.json) · [Errors](/api/errors) · [Rate limits](/api/rate-limits) ## MCP server - **Endpoint:** `https://mcp.actuallycare.com/mcp` (Streamable HTTP) - **Auth:** OAuth 2.1 with PKCE and dynamic client registration — clients discover everything from the 401 `WWW-Authenticate` header. An ActuallyCare login is required at the consent screen. - **Connect from Claude:** Settings → Connectors → Add custom connector → paste the endpoint URL. Walkthrough: [Connect Claude in 5 minutes](/mcp/quickstart). - **Tool index:** [tools.json](https://docs.actuallycare.com/tools.json) — names, descriptions, input schemas, safety annotations for every tool. ## Webhooks Register via `POST /v1/webhooks` (JWT bearer auth, broker or system-admin role); deliveries are HMAC-signed. Setup and verification: [Set up webhooks](/guides/webhook-setup). ## Machine-readable docs [llms.txt](https://docs.actuallycare.com/llms.txt) · [llms-full.txt](https://docs.actuallycare.com/llms-full.txt) · append `.md` to any page URL · [.well-known/ai.json](https://docs.actuallycare.com/.well-known/ai.json) --- # REST API quickstart > Get an ActuallyCare API key and make your first authenticated request in about two minutes. The ActuallyCare REST API lives at `https://api.actuallycare.com/v1`. You authenticate with an API key in the `X-API-Key` header, and every response comes back in the same JSON envelope. Here's the whole loop: ```bash title="cURL" curl "https://api.actuallycare.com/v1/listings" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings", { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY }, }); const body = await res.json(); console.log(`Fetched ${body.data.listings.length} of ${body.data.meta.total} listings`); ``` ```python title="Python" import os import requests resp = requests.get( "https://api.actuallycare.com/v1/listings", headers={"X-API-Key": os.environ["ACTUALLYCARE_API_KEY"]}, ) body = resp.json() print(f"Fetched {len(body['data']['listings'])} of {body['data']['meta']['total']} listings") ``` ```json { "success": true, "data": { "listings": [ { "id": "0d4f8a9c-3b2e-4f6a-9d1c-7e5b2a8f4c10", "address": "456 Oak Ave, Tehachapi, CA 93561", "status": "active", "list_price": 425000 } ], "meta": { "page": 1, "limit": 25, "total": 1, "totalPages": 1, "hasMore": false } }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` Keep your key in an environment variable — never in source code or anything that ships to a browser. The rest of this page gets you from zero to that response. ## Get an API key 1. Sign in at [app.actuallycare.com](https://app.actuallycare.com) and open **Settings → API Keys** (direct link: [app.actuallycare.com/settings?tab=api](https://app.actuallycare.com/settings?tab=api)). Every user can create their own keys — each key acts as you, with your role's visibility. You need an **active** account: self-registration currently lands on a waitlist, and waitlisted accounts get `403 ACCOUNT_NOT_ACTIVE` on every endpoint (including key creation) until approved. 2. Click to create a key, give it a name that says what it's for ("Reporting script", "Zapier bridge"), choose an expiration, and grant it the scopes it needs — scopes are what give a key access, and a key with no scopes gets `403` on everything. 3. Copy the key immediately. It's a 64-character hex string, shown exactly once — ActuallyCare stores only a hash, and the UI shows just the first 8 and last 4 characters afterward. Keys can also be created, listed, and revoked over the API itself — see [Authentication](/api/authentication) for the details, including how scopes grant each key its access. ## The response envelope Every successful response has the same shape: `success` is `true`, `data` holds the result, and `timestamp` is the server time. On list endpoints `data` is an object keyed by the resource (`data.listings`, `data.clients`, …) with pagination metadata at `data.meta` — see [Pagination](/api/pagination). When something goes wrong, `success` flips to `false` and an `error` object explains why — see [Errors](/api/errors). ## What the API covers The documented REST surface today covers Authentication, API Keys, Contacts, Clients, Leads, Escrows, Listings, Appointments, Webhooks, and Billing. If you need data the REST API doesn't expose, the MCP server covers far more of the platform — see [API vs MCP](/concepts/api-vs-mcp) for how to choose. There's also a machine-readable OpenAPI spec at `https://api.actuallycare.com/v1/openapi.json` — useful for generating clients or importing endpoints into your own tooling — and an interactive Swagger UI at [api.actuallycare.com/v1/api-docs](https://api.actuallycare.com/v1/api-docs) for trying endpoints in the browser. ## Where next | You want to | Go to | | --- | --- | | Understand API keys, JWTs, and 401 vs 403 | [Authentication](/api/authentication) | | Page through large result sets | [Pagination](/api/pagination) | | Handle errors and retries properly | [Errors](/api/errors) | | Stay under the request limits | [Rate limits](/api/rate-limits) | | Browse every endpoint, parameter, and field | [API reference](/api/reference) | | React to changes instead of polling | [Webhooks](/concepts/webhooks) | --- # Authentication > API keys and JWT bearer tokens for the ActuallyCare REST API, plus how 401 differs from 403. Most integrations authenticate with an API key in the `X-API-Key` header: ```bash curl "https://api.actuallycare.com/v1/clients" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json { "success": true, "data": { "clients": [], "meta": { "page": 1, "limit": 25, "total": 0, "totalPages": 0, "hasMore": false } }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` A `200` with the standard envelope means your key works **and has the scope this endpoint needs** — a key with no scopes gets `403` on every endpoint, so scopes are part of "working" (see [Scopes](#scopes) below). The rest of this page covers API keys in depth, then JWT auth for apps where users sign in directly. A few endpoint groups take **only** a JWT bearer token — an `X-API-Key` header gets `401 No authentication token provided` there: key management (`/v1/api-keys`), webhooks (`/v1/webhooks`), contacts (`/v1/contacts`), and billing (`/v1/billing`). Each page in the [API reference](/api/reference) shows the auth its endpoints actually accept. ## API keys ### The header Send the key on every request in the `X-API-Key` header. The header name `API-Key` also works, but `X-API-Key` is the documented form — use it in new code. ### Key format Keys are 64-character hex strings with no prefix: ```text 9f2c4e8a1b3d5f7092c4e6a8b0d2f4169e8c0a2b4d6f8e1a3c5b7d9f0e2a4c6b ``` The full key is shown exactly once, at creation. ActuallyCare stores only a hash, so it can never be retrieved again — if you lose it, create a new key. After creation the UI shows just the first 8 and last 4 characters so you can tell keys apart. ### Create a key in the app 1. Sign in at [app.actuallycare.com](https://app.actuallycare.com) and open **Settings → API Keys** ([app.actuallycare.com/settings?tab=api](https://app.actuallycare.com/settings?tab=api)). Every user can create their own keys — each key acts as you, with your role's visibility. 2. Name the key after the integration it serves, pick an expiration (the UI also allows 0, meaning no expiration), and create it. 3. Copy the key before closing the dialog. Make sure the key is granted the scopes it needs — a key with no scopes can't access anything (see [Scopes](#scopes)). ### Create a key over the API `POST /v1/api-keys` creates a key programmatically. Key management authenticates with a **JWT bearer token** (sign in first — see [JWT bearer tokens](#jwt-bearer-tokens) below), not with an API key: ```bash curl -X POST "https://api.actuallycare.com/v1/api-keys" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Reporting integration", "expiresInDays": 90, "scopes": { "all": ["read", "write"] } }' ``` ```json { "success": true, "data": { "id": "5c2e7f1a-8b4d-4e6f-9a0c-1d3e5f7a9b2c", "key": "9f2c4e8a1b3d5f7092c4e6a8b0d2f4169e8c0a2b4d6f8e1a3c5b7d9f0e2a4c6b", "name": "Reporting integration", "expires_at": "2026-09-09T18:24:09.123Z" }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` `name` is required. `expiresInDays` is optional — 1 to 365 days. **If you omit it, the key never expires** — there's no implicit default, so set an expiry explicitly. `scopes` is what makes the key usable at all (next section). The `data.key` field in the response is the only time you'll see the full key. The full set of key-management endpoints (list, revoke, delete) is in the [API Keys reference](/api/reference/api-keys). ### Scopes Scopes **grant** access — a key has no permissions except the ones its scopes spell out. A key created without a scopes object has an empty scope set and gets `403` ("API key missing required scope") on every documented endpoint. Always include scopes when you create a key: - `{"all":["read"]}` — read access everywhere the key's user can see - `{"all":["read","write"]}` — full read and write access - `{"clients":["read","write"]}` — per-resource grants; this key can only touch clients Grants combine, so `{"all":["read"],"clients":["read","write"]}` means read everywhere plus write on clients. You can also grant or change scopes after creation with `PATCH /v1/api-keys/:id/scopes`: ```bash curl -X PATCH "https://api.actuallycare.com/v1/api-keys/KEY_ID/scopes" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "scopes": { "all": ["read"], "clients": ["read", "write"] } }' ``` Give each key the narrowest grant that still works. Scopes are an API-key concept — JWT-authenticated users aren't scope-checked; they act with their normal role and permissions. ### Rotate keys Rotation is a three-step swap, with no downtime: 1. Create a new key. 2. Update the integration to use it and confirm it works. 3. Revoke the old key (in the UI, or via the [API Keys endpoints](/api/reference/api-keys)). Use one key per integration so you can rotate or revoke each one independently, and revoke immediately if a key ever leaks. A revoked key fails with `401` on the next request. ### Store keys safely - Keep keys in environment variables or a secrets manager — never in source code, and never committed to git. - Never ship a key in client-side code (browser bundles, mobile apps). Anything a user can download can be read. - Don't log keys, and don't paste them into chat tools or tickets. ```bash export ACTUALLYCARE_API_KEY="9f2c4e8a1b3d5f7092c4e6a8b0d2f4169e8c0a2b4d6f8e1a3c5b7d9f0e2a4c6b" ``` ```javascript const res = await fetch("https://api.actuallycare.com/v1/clients", { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY }, }); ``` ## JWT bearer tokens If you're building an app where ActuallyCare users sign in with their own email and password, use JWT auth instead of a shared API key — each user acts with their own permissions. ### Log in ```bash curl -X POST "https://api.actuallycare.com/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{ "email": "agent@example.com", "password": "their-password" }' ``` The `email` field also accepts a username — `{ "username": "agent1", "password": "..." }` works the same way (that's why a failed login says "Invalid username or password"). ```json { "success": true, "data": { "user": { "id": "ab12cd34-..." }, "accessToken": "eyJhbGciOiJIUzI1NiIs...", "refreshToken": "f3a1c9e7...", "expiresIn": "15m", "tokenType": "Bearer" }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` Send the access token on requests as a bearer token: ```bash curl "https://api.actuallycare.com/v1/clients" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." ``` ### Other login outcomes A `200` from `/auth/login` isn't always a ready-to-use session — branch on the response shape: - **Two-factor enabled:** the response carries `data.requiresTOTP: true` (or `data.requiresSMS: true`) plus `data.userId`, and no token. Complete the challenge on the corresponding verification endpoint before you get a JWT. - **Waitlisted account:** newly registered accounts start on a waitlist. Login succeeds with `data.status: "waitlisted"` and a token, but that token can only call `/auth/verify` — every data and API-key endpoint returns `403 ACCOUNT_NOT_ACTIVE` until the account is approved. The response's `data.redirect` points at the waitlist page. - **Account locked:** five consecutive failed logins lock the account for **30 minutes** and send an alert email to the account owner. While locked, login returns HTTP `423` with the code `ACCOUNT_LOCKED` and `error.details.locked_until`. Stop retrying and wait it out — more attempts extend nothing but the frustration. ### Token lifetimes and refresh Access-token lifetime is **role-based** — anywhere from 15 minutes to 8 hours depending on the user's role. Don't hardcode an expiry, and note that `expiresIn` is a **duration string** like `"15m"`, `"4h"`, or `"8h"` — not a number of seconds. Parse the string, or decode the JWT's `exp` claim, and refresh before the token runs out. To refresh, call `POST /v1/auth/refresh` with the refresh token. Each refresh **rotates** the refresh token — store the new one and discard the old. Sessions stay alive on a 30-day sliding window, with a 90-day absolute cap; after that the user signs in again. ### One token, both servers The same JWT also authenticates against the MCP server at `https://mcp.actuallycare.com/mcp`, so an app that manages user sessions can call REST endpoints and MCP tools with a single credential. See [Build a custom MCP app](/mcp/custom-apps). ## 401 vs 403 | Status | What it means | Typical causes | | --- | --- | --- | | `401` | The request wasn't authenticated | Missing `X-API-Key` or `Authorization` header, a typo'd or revoked key, an expired key, an expired JWT | | `403` | Authenticated, but not allowed | The endpoint is role-gated, the record belongs to another team, or the key is missing the required scope (a key with no scopes fails everywhere) | The practical rule: a `401` means fix your credential; a `403` means fix your permissions — retrying won't help either one. Both come back in the standard error envelope described in [Errors](/api/errors). --- # Errors > The ActuallyCare error envelope, HTTP status codes, and when (and when not) to retry. Every error from the API comes back in the same envelope — `success` flips to `false` and an `error` object explains what happened. An illustrative example: ```json { "success": false, "error": { "code": "NOT_FOUND", "message": "Client not found." }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` | Field | What it is | | --- | --- | | `success` | Always `false` on errors | | `error.code` | Machine-readable code in SCREAMING_SNAKE_CASE (like `NOT_FOUND` or `VALIDATION_ERROR`) — stable, safe to branch on | | `error.message` | Human-readable explanation — wording can change, so never string-match it in code | | `error.details` | Optional extra context: an array of per-field problems on validation errors, or an object (for example `locked_until` on an account lockout) | | `errorId`, `correlationId` | Optional identifiers on some errors — include them when contacting support so the failure can be traced | | `timestamp` | Server time of the failure | Login failures return stable snake-case codes: `INVALID_CREDENTIALS` (401 — unknown user or wrong password), `ACCOUNT_DISABLED` (401), `ACCOUNT_SUSPENDED` (403), and `ACCOUNT_LOCKED` (423, with `error.details.locked_until`). One narrower deviation remains: token-layer failures — a missing or expired bearer token, or an invalid API key — still return class-style codes like `UnauthorizedError`. Branch on the HTTP status for those until they're normalized. ## HTTP status codes | Status | Meaning | What to do | | --- | --- | --- | | `200` | Success | Read `data` from the envelope | | `201` | Created | Read `data` for the new record | | `400` | Bad request — malformed JSON or invalid parameters | Fix the request; check `error.message` | | `401` | Not authenticated | Check the `X-API-Key` header or token — see [Authentication](/api/authentication) | | `403` | Authenticated but not allowed | A permissions problem (role, team, or key scope) — retrying won't help. Also returned as `ACCOUNT_NOT_ACTIVE` when a waitlisted account isn't approved yet, and `ACCOUNT_SUSPENDED` for suspended accounts | | `404` | Not found | Check the ID (all IDs are UUIDs) and that the record belongs to your team | | `409` | Conflict | The request clashes with current state (a duplicate, or a stale update) — re-fetch and decide deliberately | | `423` | Account locked | Returned as `ACCOUNT_LOCKED`. Five consecutive failed logins lock the account for 30 minutes; `error.details.locked_until` says when it reopens. Stop retrying — see [Authentication](/api/authentication) | | `429` | Rate limited | Back off and retry — see [Rate limits](/api/rate-limits) | | `5xx` | Server error | Retry with backoff; check [status.actuallycare.com](https://status.actuallycare.com) if it persists | ## Retry on 429 and 5xx — and nothing else A `429` or `5xx` is transient: waiting and retrying usually succeeds. Every other `4xx` is deterministic — the same request will fail the same way, so retrying just burns rate limit. Fix the request instead. Use exponential backoff with a little jitter: ```javascript async function withRetry(makeRequest, maxRetries = 3) { for (let attempt = 0; ; attempt++) { const res = await makeRequest(); if (res.ok) return res.json(); const retryable = res.status === 429 || res.status >= 500; if (!retryable || attempt === maxRetries) { const body = await res.json().catch(() => null); throw new Error(body?.error?.message ?? `HTTP ${res.status}`); } // 1s, 2s, 4s... plus jitter so parallel clients don't retry in lockstep const delay = 2 ** attempt * 1000 + Math.random() * 250; await new Promise((resolve) => setTimeout(resolve, delay)); } } const listings = await withRetry(() => fetch("https://api.actuallycare.com/v1/listings", { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY }, }) ); ``` ## A 429 in practice Rate-limited responses use the standard envelope with the code `RATE_LIMIT_EXCEEDED`, plus `RateLimit-*` and `Retry-After` headers that tell you exactly when to come back: ```json { "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", "message": "Too many requests, please try again later.", "errorId": "err_8f3a1c" } } ``` Branch on the status (or `error.code`), wait out `Retry-After` (seconds) or `RateLimit-Reset`, and retry. The `errorId` is a server-side trace id — include it if you contact support about a stuck limit. Guarding `res.json()` on non-2xx responses is still good hygiene (proxies and load balancers can produce non-JSON bodies), but the API itself answers `429` in JSON. The current limits, and a retry snippet that reads `RateLimit-Reset` instead of guessing, are on the [Rate limits](/api/rate-limits) page. ## Debugging tips - **Start with the HTTP status, then `error.code`.** The code is the stable, machine-readable answer to "what went wrong" — branch on it, not on the message. - **Read `error.message` as a human.** It usually names the exact field or rule that failed. - **Most 401s are credential problems.** A missing header, a typo, or a revoked or expired key — walk through [Authentication](/api/authentication) before suspecting anything else. - **On 429s, check the `RateLimit-*` headers.** They tell you your limit, what's left, and how many seconds until the window resets. - **Use the `timestamp` field.** It's the server's clock at the moment of failure — handy for lining errors up with your own logs, and worth including when you contact support. - **On 400s, check the request body against the reference.** Required fields and types for every endpoint are in the [API reference](/api/reference). --- # Pagination > Page through list endpoints with the page and limit query parameters. Every list endpoint takes the same two query parameters — `page` and `limit` — and returns the records under `data`, keyed by the resource name, with pagination metadata at `data.meta`: ```bash curl "https://api.actuallycare.com/v1/clients?page=2&limit=50" \ -H "X-API-Key: YOUR_API_KEY" ``` ```json { "success": true, "data": { "clients": [ { "id": "7b1e9c44-2f6a-4d83-9c0e-5a1f2b3c4d5e" } ], "meta": { "page": 2, "limit": 50, "total": 137, "totalPages": 3, "hasMore": true } }, "timestamp": "2026-06-11T18:24:09.123Z" } ``` The entity key matches the resource — `data.clients` here, `data.leads` on `/v1/leads`, `data.listings` on `/v1/listings`, `data.appointments` on `/v1/appointments`. (Records trimmed for brevity — each entry is a full object; the per-endpoint field lists are in the [API reference](/api/reference).) ## Parameters | Parameter | Type | Default | Notes | | --- | --- | --- | --- | | `page` | integer | `1` | Which page to fetch, starting at 1 | | `limit` | integer | `25` | Records per page, maximum 100 | Stick to the parameters documented for each endpoint in the [API reference](/api/reference) — don't invent sort or filter parameters that aren't listed there. ## The meta object | Field | Meaning | | --- | --- | | `page` | The page you got back | | `limit` | Records per page for this response | | `total` | Total matching records across all pages | | `totalPages` | Total number of pages at this `limit` | | `hasMore` | `true` while there are more pages to fetch | You're done when `hasMore` is `false` — or, defensively, when a page comes back empty. ## Fetch every page Both loops below request 100 records at a time (the maximum) and stop when `data.meta.hasMore` goes `false`. JavaScript: ```javascript async function fetchAll(path, entity) { const all = []; let page = 1; let hasMore = true; while (hasMore) { const res = await fetch( `https://api.actuallycare.com/v1${path}?page=${page}&limit=100`, { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY } } ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const body = await res.json(); all.push(...body.data[entity]); hasMore = body.data.meta.hasMore; page += 1; } return all; } const clients = await fetchAll("/clients", "clients"); ``` Python: ```python import os import requests def fetch_all(path, entity): records = [] page = 1 has_more = True while has_more: res = requests.get( f"https://api.actuallycare.com/v1{path}", headers={"X-API-Key": os.environ["ACTUALLYCARE_API_KEY"]}, params={"page": page, "limit": 100}, ) res.raise_for_status() body = res.json() records.extend(body["data"][entity]) has_more = body["data"]["meta"]["hasMore"] page += 1 return records clients = fetch_all("/clients", "clients") ``` ## Paginate less The fastest page is the one you don't fetch: - **Filter server-side where filters exist.** For example, `GET /v1/listings` accepts `status`, `minPrice`, and `maxPrice` — filtering there beats downloading everything and filtering in your code. Each endpoint's filters are in [the listings reference](/api/reference/listings). - **Mind the rate limit.** Every page is one request against your [rate limit](/api/rate-limits), so a full export at `limit=100` costs a request per hundred records. Cache results you'll reuse. - **Don't poll for changes.** If you're re-fetching everything on a schedule just to spot what changed, subscribe to [webhooks](/concepts/webhooks) instead and let ActuallyCare push the changes to you. --- # Rate limits > Request limits for the REST API and MCP server, and how to handle a 429 cleanly. Every response from the API tells you where you stand. Make any request with `-i` and read the headers: ```bash curl -i "https://api.actuallycare.com/v1/listings" \ -H "X-API-Key: YOUR_API_KEY" ``` ```text HTTP/2 200 RateLimit-Limit: 500 RateLimit-Remaining: 499 RateLimit-Reset: 893 ``` `RateLimit-Limit` is your budget for the window, `RateLimit-Remaining` is what's left, and `RateLimit-Reset` is the number of seconds until the window resets. These are the standard `RateLimit-*` headers — not the older `X-RateLimit-*` convention, so check the exact names if your HTTP library helpfully "normalizes" headers. ## The limits | Scope | Limit | Window | | --- | --- | --- | | All `/v1` endpoints | 500 requests per IP | 15 minutes | | Auth endpoints (`/v1/auth`, excluding `/refresh`) | 30 **failed** attempts per IP — successful requests don't count | 15 minutes | | `POST /v1/auth/login` (additional cap) | 50 **total** attempts per IP — successes included | 15 minutes | | `/v1/auth/refresh` | 30 requests per IP | 1 minute | Limits apply per IP address. Two limits stack on login: the 30-failed-attempts budget is the one a broken integration hits first, and a second 50-total-attempts cap protects the endpoint even from successful-login loops. The `RateLimit-*` headers on a login response report the **50-total** cap (`RateLimit-Limit: 50`) because it's the last limiter in the chain — the 30-failed budget is enforced but not surfaced in headers. If you're hitting either one, something is failing repeatedly, or you're logging in per request instead of reusing tokens. Authenticate once and refresh; see [Authentication](/api/authentication). ## When you hit 429 A rate-limited request returns the standard error envelope with `error.code` set to `RATE_LIMIT_EXCEEDED` (see [Errors](/api/errors)), along with the `RateLimit-*` headers above and a `Retry-After` header in seconds. The polite retry honors `Retry-After` (falling back to `RateLimit-Reset`) instead of guessing: ```javascript async function getJSON(url, attempt = 0) { const res = await fetch(url, { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY }, }); if (res.status === 429 && attempt < 3) { const wait = Number(res.headers.get("Retry-After")) || Number(res.headers.get("RateLimit-Reset")) || 2 ** attempt * 5; await new Promise((resolve) => setTimeout(resolve, wait * 1000)); return getJSON(url, attempt + 1); } if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`); return res.json(); } ``` Retry only on `429` and `5xx` — other errors won't get better with repetition. The full backoff pattern is on the [Errors](/api/errors) page. ## Stay under the limits - **Cache reads.** If a script asks for the same listings three times in one run, that's two wasted requests. Fetch once, reuse. - **Use webhooks instead of polling.** Re-fetching everything on a timer to spot changes is the fastest way to burn 500 requests. Subscribe to [webhooks](/concepts/webhooks) and let ActuallyCare push changes to you. - **Fetch big lists at `limit=100`.** One page of 100 costs a fifth of five pages of 20 — see [Pagination](/api/pagination). - **Spread out scheduled jobs.** A cron job that fans out dozens of parallel requests at the top of the hour competes with itself. Stagger the work. ## MCP server limits If you're building a custom MCP client, the MCP server has its own per-user limits, separate from the REST limits above: | Scope | Limit | | --- | --- | | All MCP requests | 120 per minute, per user | | Tool calls | 30 per minute, per user | Tool executions can also run for up to 300 seconds before timing out, so set your client timeouts accordingly. See [Build a custom MCP app](/mcp/custom-apps) for the rest of the operational details. --- # Endpoint reference > Every REST endpoint on api.actuallycare.com — 64 operations across 10 resource groups. The REST API exposes **64 operations** across **10 resource groups**. All requests use the base URL `https://api.actuallycare.com/v1` and authenticate with an [API key or JWT](/api/authentication). This reference is generated from the live OpenAPI spec. Machine-readable version: [`openapi.json`](https://api.actuallycare.com/v1/openapi.json). ## [API Keys](/api/reference/api-keys) | Endpoint | Summary | | --- | --- | | `GET /v1/api-keys` | [List API keys](/api/reference/api-keys#list-api-keys) | | `POST /v1/api-keys` | [Create new API key](/api/reference/api-keys#create-new-api-key) | | `PATCH /v1/api-keys/{id}/scopes` | [Update API key scopes](/api/reference/api-keys#update-api-key-scopes) | | `PUT /v1/api-keys/{id}/revoke` | [Revoke API key](/api/reference/api-keys#revoke-api-key) | | `DELETE /v1/api-keys/{id}` | [Delete API key](/api/reference/api-keys#delete-api-key) | ## [Appointments](/api/reference/appointments) | Endpoint | Summary | | --- | --- | | `GET /v1/appointments` | [List all appointments](/api/reference/appointments#list-all-appointments) | | `POST /v1/appointments` | [Create new appointment](/api/reference/appointments#create-new-appointment) | | `GET /v1/appointments/{id}` | [Get appointment by ID](/api/reference/appointments#get-appointment-by-id) | | `PUT /v1/appointments/{id}` | [Update appointment](/api/reference/appointments#update-appointment) | | `DELETE /v1/appointments/{id}` | [Delete appointment](/api/reference/appointments#delete-appointment) | ## [Authentication](/api/reference/authentication) | Endpoint | Summary | | --- | --- | | `POST /v1/auth/register` | [Register new user](/api/reference/authentication#register-new-user) | | `POST /v1/auth/login` | [Login user](/api/reference/authentication#login-user) | | `POST /v1/auth/refresh` | [Refresh access token](/api/reference/authentication#refresh-access-token) | | `POST /v1/auth/logout` | [Logout user](/api/reference/authentication#logout-user) | | `GET /v1/auth/verify` | [Verify token validity](/api/reference/authentication#verify-token-validity) | | `GET /v1/auth/profile` | [Get user profile](/api/reference/authentication#get-user-profile) | | `PUT /v1/auth/profile` | [Update user profile](/api/reference/authentication#update-user-profile) | | `POST /v1/auth/logout-all` | [Logout from all devices](/api/reference/authentication#logout-from-all-devices) | | `GET /v1/auth/sessions` | [List active sessions](/api/reference/authentication#list-active-sessions) | ## [Billing](/api/reference/billing) | Endpoint | Summary | | --- | --- | | `GET /v1/billing/public-plans` | [Get public plan pricing](/api/reference/billing#get-public-plan-pricing) | | `POST /v1/billing/public-checkout` | [Start checkout without an account](/api/reference/billing#start-checkout-without-an-account) | | `GET /v1/billing/plans` | [Get plans with checkout identifiers](/api/reference/billing#get-plans-with-checkout-identifiers) | | `GET /v1/billing/subscription` | [Get current subscription status](/api/reference/billing#get-current-subscription-status) | | `POST /v1/billing/checkout` | [Start checkout for a subscription](/api/reference/billing#start-checkout-for-a-subscription) | | `POST /v1/billing/portal` | [Open the billing portal](/api/reference/billing#open-the-billing-portal) | ## [Clients](/api/reference/clients) | Endpoint | Summary | | --- | --- | | `GET /v1/clients` | [List all clients](/api/reference/clients#list-all-clients) | | `POST /v1/clients` | [Create new client](/api/reference/clients#create-new-client) | | `GET /v1/clients/{id}` | [Get client by ID](/api/reference/clients#get-client-by-id) | | `PUT /v1/clients/{id}` | [Update client](/api/reference/clients#update-client) | | `DELETE /v1/clients/{id}` | [Delete client](/api/reference/clients#delete-client) | ## [Contacts](/api/reference/contacts) | Endpoint | Summary | | --- | --- | | `GET /v1/contacts` | [List contacts](/api/reference/contacts#list-contacts) | | `POST /v1/contacts` | [Create a contact](/api/reference/contacts#create-a-contact) | | `GET /v1/contacts/search` | [Search contacts](/api/reference/contacts#search-contacts) | | `GET /v1/contacts/{id}` | [Get a contact](/api/reference/contacts#get-a-contact) | | `PUT /v1/contacts/{id}` | [Update a contact](/api/reference/contacts#update-a-contact) | | `DELETE /v1/contacts/{id}` | [Delete a contact](/api/reference/contacts#delete-a-contact) | | `PATCH /v1/contacts/{id}/archive` | [Archive a contact](/api/reference/contacts#archive-a-contact) | | `PATCH /v1/contacts/{id}/restore` | [Restore an archived contact](/api/reference/contacts#restore-an-archived-contact) | ## [Escrows](/api/reference/escrows) | Endpoint | Summary | | --- | --- | | `GET /v1/escrows` | [List escrows](/api/reference/escrows#list-escrows) | | `POST /v1/escrows` | [Create an escrow](/api/reference/escrows#create-an-escrow) | | `GET /v1/escrows/{id}` | [Get an escrow](/api/reference/escrows#get-an-escrow) | | `PUT /v1/escrows/{id}` | [Update an escrow](/api/reference/escrows#update-an-escrow) | | `DELETE /v1/escrows/{id}` | [Delete an escrow](/api/reference/escrows#delete-an-escrow) | | `PATCH /v1/escrows/{id}/archive` | [Archive an escrow](/api/reference/escrows#archive-an-escrow) | | `PATCH /v1/escrows/{id}/restore` | [Restore an archived escrow](/api/reference/escrows#restore-an-archived-escrow) | | `GET /v1/escrows/{id}/timeline` | [Get an escrow's timeline](/api/reference/escrows#get-an-escrows-timeline) | ## [Leads](/api/reference/leads) | Endpoint | Summary | | --- | --- | | `GET /v1/leads` | [List all leads](/api/reference/leads#list-all-leads) | | `POST /v1/leads` | [Create new lead](/api/reference/leads#create-new-lead) | | `GET /v1/leads/{id}` | [Get lead by ID](/api/reference/leads#get-lead-by-id) | | `PUT /v1/leads/{id}` | [Update lead](/api/reference/leads#update-lead) | | `DELETE /v1/leads/{id}` | [Delete lead](/api/reference/leads#delete-lead) | | `POST /v1/leads/{id}/convert` | [Convert lead to client](/api/reference/leads#convert-lead-to-client) | ## [Listings](/api/reference/listings) | Endpoint | Summary | | --- | --- | | `GET /v1/listings` | [List all property listings](/api/reference/listings#list-all-property-listings) | | `POST /v1/listings` | [Create new listing](/api/reference/listings#create-new-listing) | | `GET /v1/listings/{id}` | [Get listing by ID](/api/reference/listings#get-listing-by-id) | | `PUT /v1/listings/{id}` | [Update listing](/api/reference/listings#update-listing) | | `DELETE /v1/listings/{id}` | [Delete listing](/api/reference/listings#delete-listing) | ## [Webhooks](/api/reference/webhooks) | Endpoint | Summary | | --- | --- | | `GET /v1/webhooks` | [List webhook subscriptions](/api/reference/webhooks#list-webhook-subscriptions) | | `POST /v1/webhooks` | [Create a webhook subscription](/api/reference/webhooks#create-a-webhook-subscription) | | `GET /v1/webhooks/events` | [List available webhook events](/api/reference/webhooks#list-available-webhook-events) | | `GET /v1/webhooks/events/categories` | [List webhook event categories](/api/reference/webhooks#list-webhook-event-categories) | | `PUT /v1/webhooks/{id}` | [Update a webhook subscription](/api/reference/webhooks#update-a-webhook-subscription) | | `DELETE /v1/webhooks/{id}` | [Deactivate a webhook subscription](/api/reference/webhooks#deactivate-a-webhook-subscription) | | `GET /v1/webhooks/{id}/logs` | [Get webhook delivery logs](/api/reference/webhooks#get-webhook-delivery-logs) | ## Error responses Every endpoint returns errors in the same envelope — `success` is `false` and an `error` object carries a stable machine-readable `code` and a human `message`. Branch on `error.code`, never the message. See [Errors](/api/errors) for the full status-code and error-code reference and retry guidance. ```json { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Invalid request data", "errorId": "ERR_1782972632_ab12cd", "correlationId": "b7e4c2a8-3f6d-4e29-9c41-8d5a2f7e1b93", "details": null }, "timestamp": "2026-07-15T14:32:10.000Z" } ``` --- # API Keys API > REST endpoints for api keys — request and response reference with examples. Create and revoke API keys programmatically, or self-serve in the app: Settings → API Keys → Create API Key. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List API keys ```http GET /v1/api-keys ``` Returns list of API keys for authenticated user (keys are masked) ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/api-keys" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/api-keys", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/api-keys", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Jordan Lee", "key_preview": "example key preview", "is_active": true, "created_at": "2026-07-15T14:32:10.000Z", "last_used_at": "2026-07-15T14:32:10.000Z", "expires_at": "2026-07-15T14:32:10.000Z" } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | API keys retrieved successfully | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create new API key ```http POST /v1/api-keys ``` Generates a new API key for authenticated user. Key is only shown once. ⚠️ **CONSEQUENTIAL ACTION**: Creates new authentication credentials. Store the key securely - it cannot be retrieved again. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | Descriptive name for the API key | | `expiresInDays` | integer | No | Number of days until expiration (1-365). If omitted, the key does not expire. | | `scopes` | object | No | Map of resource → allowed actions (an OBJECT, not a string array), e.g. {"escrows": ["read", "write"], "clients": ["read"]}. Use resource "all" as a wildcard (e.g. {"all": ["read"]}). Valid resources: all, escrows, clients, contacts, leads, appointments, listings, documents, open-houses, showings, details, tasks, notes, communications, emails, integrations, settings, analytics, activity, notifications, users, search, teams, vendor-applications. **If omitted, the key is created with {} (no scopes) and is DENIED (403) on every scoped resource** — pass scopes here or grant them later via PATCH /api-keys/{id}/scopes. | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/api-keys" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "Jordan Lee", "scopes": { "all": [ "read" ], "escrows": [ "read", "write" ] } }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/api-keys", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Jordan Lee", "scopes": { "all": [ "read" ], "escrows": [ "read", "write" ] } }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/api-keys", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "name": "Jordan Lee", "scopes": { "all": [ "read" ], "escrows": [ "read", "write" ] } }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "key": "example key", "name": "Jordan Lee", "key_prefix": "example key prefix", "scopes": {}, "expires_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" }, "message": "example message", "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | API key created successfully | | `400` | Validation error | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update API key scopes ```http PATCH /v1/api-keys/{id}/scopes ``` Replaces the scopes object on an existing API key. Requires JWT authentication (API keys cannot manage API keys). Scopes are a map of resource → allowed actions; see POST /api-keys for the valid resource list. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `scopes` | object | Yes | Full replacement scopes map (resource → actions) | ### Example request ```bash title="cURL" curl -X PATCH "https://api.actuallycare.com/v1/api-keys/:id/scopes" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "scopes": { "escrows": [ "read", "write" ], "all": [ "read" ] } }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/api-keys/:id/scopes", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "scopes": { "escrows": [ "read", "write" ], "all": [ "read" ] } }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.patch( "https://api.actuallycare.com/v1/api-keys/:id/scopes", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "scopes": { "escrows": [ "read", "write" ], "all": [ "read" ] } }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "7d3f9b2e-5a8c-4e1d-b6f0-9c2a4e8d1b57", "scopes": { "escrows": [ "read", "write" ], "all": [ "read" ] } }, "timestamp": "2026-07-01T22:48:31.554Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Scopes updated | | `400` | Validation error (scopes must be an object) | | `401` | Unauthorized | | `404` | API key not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Revoke API key ```http PUT /v1/api-keys/{id}/revoke ``` Deactivates an API key without deleting it ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Format: uuid | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/api-keys/:id/revoke" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/api-keys/:id/revoke", { method: "PUT", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/api-keys/:id/revoke", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "7d3f9b2e-5a8c-4e1d-b6f0-9c2a4e8d1b57" }, "timestamp": "2026-07-01T22:53:07.114Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | API key revoked successfully | | `401` | Unauthorized | | `404` | API key not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete API key ```http DELETE /v1/api-keys/{id} ``` Permanently deletes an API key ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/api-keys/:id" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/api-keys/:id", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/api-keys/:id", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": {}, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | API key deleted successfully | | `401` | Unauthorized | | `404` | API key not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Appointments API > REST endpoints for appointments — request and response reference with examples. Calendar events — create, list, update, and cancel appointments. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List all appointments ```http GET /v1/appointments ``` Returns paginated list of appointments/showings. Default page size 25 (max 100). ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `appointment_type` | query | enum | No | One of: `showing`, `inspection`, `signing`, `meeting`, `call`, `other` | | `status` | query | enum | No | One of: `scheduled`, `completed`, `cancelled`, `no_show` | | `start_date` | query | string | No | Format: date | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/appointments" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/appointments", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/appointments", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "appointments": [ { "id": "d4b7a9e2-8c1f-4d6a-b3e5-7f9c2a0d8b46", "title": "Buyer consultation — Torres family", "appointment_type": "meeting", "start_time": "2026-07-10T17:00:00Z", "end_time": "2026-07-10T18:00:00Z", "location": "9900 Stockdale Hwy, Bakersfield, CA 93311", "client_id": "a3f8c1d2-6b4e-4a9f-8d27-95c0e3b1f684", "status": "scheduled" } ], "meta": { "page": 1, "limit": 25, "total": 12, "totalPages": 1, "hasMore": false } }, "timestamp": "2026-07-01T22:50:41.207Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated list — records under data.appointments, pagination under data.meta | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create new appointment ```http POST /v1/appointments ``` ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | Yes | Appointment title | | `start_time` | string | Yes | Appointment start time (ISO 8601) · Format: date-time | | `end_time` | string | Yes | Appointment end time (ISO 8601) · Format: date-time | | `appointment_type` | string | No | Type of appointment (showing, inspection, signing, meeting, call, other) | | `location` | string | No | Appointment location | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/appointments" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "your-title", "start_time": "your-start-time", "end_time": "your-end-time" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/appointments", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "title": "your-title", "start_time": "your-start-time", "end_time": "your-end-time" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/appointments", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "title": "your-title", "start_time": "your-start-time", "end_time": "your-end-time" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "d4b7a9e2-8c1f-4d6a-b3e5-7f9c2a0d8b46", "title": "Showing — 456 Oak Ave", "appointment_type": "showing", "start_time": "2026-07-12T21:30:00Z", "end_time": "2026-07-12T22:00:00Z", "location": "456 Oak Ave, Tehachapi, CA 93561", "status": "scheduled", "version": 1 }, "message": "Appointment created successfully", "timestamp": "2026-07-01T22:51:12.940Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Appointment created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get appointment by ID ```http GET /v1/appointments/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/appointments/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/appointments/:id", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/appointments/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "d4b7a9e2-8c1f-4d6a-b3e5-7f9c2a0d8b46", "title": "Buyer consultation — Torres family", "appointment_type": "meeting", "start_time": "2026-07-10T17:00:00Z", "end_time": "2026-07-10T18:00:00Z", "location": "9900 Stockdale Hwy, Bakersfield, CA 93311", "client_id": "a3f8c1d2-6b4e-4a9f-8d27-95c0e3b1f684", "status": "scheduled", "version": 1 }, "timestamp": "2026-07-01T22:51:44.518Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Appointment found | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update appointment ```http PUT /v1/appointments/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `start_time` | string | No | Appointment start time (ISO 8601) · Format: date-time | | `end_time` | string | No | Appointment end time (ISO 8601) · Format: date-time | | `status` | string | No | Appointment status (scheduled, completed, cancelled, no_show) | | `version` | integer | No | Current record version for optimistic locking | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/appointments/:id" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "start_time": "your-start-time", "end_time": "your-end-time", "status": "your-status", "version": 3 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/appointments/:id", { method: "PUT", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "start_time": "your-start-time", "end_time": "your-end-time", "status": "your-status", "version": 3 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/appointments/:id", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "start_time": "your-start-time", "end_time": "your-end-time", "status": "your-status", "version": 3 }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "title": "Property Showing - 123 Main St", "description": "example description", "appointment_type": "showing", "start_time": "2025-02-01T14:00:00Z", "end_time": "2025-02-01T15:00:00Z", "location": "123 Main St, Tehachapi, CA 93561", "client_id": "550e8400-e29b-41d4-a716-446655440000", "listing_id": "550e8400-e29b-41d4-a716-446655440000", "escrow_id": "550e8400-e29b-41d4-a716-446655440000", "status": "scheduled", "attendees": [ { "name": "Jordan Lee", "email": "agent@example.com", "phone": "+1 661 555 0123" } ], "reminder_sent": true, "notes": "example notes", "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" }, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Appointment updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete appointment ```http DELETE /v1/appointments/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/appointments/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/appointments/:id", { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/appointments/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": {}, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Appointment deleted | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Authentication API > REST endpoints for authentication — request and response reference with examples. Session management for user-facing apps: register, log in, refresh tokens, and manage sessions. If you are building a server-side integration, you usually want an [API key](/api/authentication) instead. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## Register new user ```http POST /v1/auth/register ``` Creates a new user account and always returns a signed JWT. **Waitlist gate:** when the platform is accepting signups (`ACCEPT_NEW_USERS=true`, or the registration arrives via a brokerage invite) the account is created with status `active` and onboarding starts immediately. Otherwise the account is created with status `waitlisted` — the returned JWT can only reach `/auth/*` and `/waitlist/*` endpoints; every other endpoint returns 403 with error.code `ACCOUNT_NOT_ACTIVE` until an admin approves the account. Both outcomes return 201 with the same shape; check `data.status` and follow `data.redirect`. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | Yes | User's email address (login identifier, stored lowercase) · Format: email | | `username` | string | Yes | Unique username — letters, numbers, and underscores only (stored lowercase) | | `password` | string | Yes | Account password — min 8 chars, at least one uppercase letter and one number | | `firstName` | string | Yes | User's first name | | `lastName` | string | Yes | User's last name | | `role` | enum | No | Optional starting role for the account · One of: `agent`, `broker`, `lender`, `vendor` · Default: `"agent"` | | `emailKind` | enum | No | Optional classification of the provided email address · One of: `personal`, `work`, `offers` | | `phone` | string | No | Optional phone number (required for SMS consent to be recorded) | | `smsConsent` | boolean | No | Optional explicit consent to receive SMS (recorded only when phone is provided) | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/auth/register" \ -H "Content-Type: application/json" \ -d '{ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "your-password", "firstName": "Sarah", "lastName": "Chen", "phone": "(661) 555-0142" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/register", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "your-password", "firstName": "Sarah", "lastName": "Chen", "phone": "(661) 555-0142" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/auth/register", json={ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "your-password", "firstName": "Sarah", "lastName": "Chen", "phone": "(661) 555-0142" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "user": { "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17", "email": "sarah.chen@gmail.com", "firstName": "Sarah", "lastName": "Chen", "role": [ "agent" ], "isActive": true, "status": "active" }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ", "status": "active", "redirect": "/get-started", "onboarding": { "sampleDataGenerated": true, "tutorialAvailable": true, "nextStep": "/onboarding/welcome" } }, "message": "User registered successfully" } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | User registered — `data.status` is `active` or `waitlisted` depending on the ACCEPT_NEW_USERS gate | | `400` | Invalid request data | | `409` | A user with this email or username already exists (case-insensitive check) | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Login user ```http POST /v1/auth/login ``` Authenticate with email OR username plus password. On full success, returns a JWT access token whose lifetime is role-based (`expiresIn` duration string: `15m` for system_admin and default, `4h` for broker/team_lead, `8h` for agent) and sets a `refreshToken` httpOnly cookie (default 30 days, `JWT_REFRESH_TOKEN_EXPIRY_DAYS`). The 200 body has four possible `data` shapes: full login, TOTP challenge (`requiresTOTP`), SMS challenge (`requiresSMS`), or waitlisted account. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | No | User's email address (provide email OR username; case-insensitive) | | `username` | string | No | Username (provide email OR username; case-insensitive) | | `password` | string | Yes | Account password | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "YourPassword123!" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "YourPassword123!" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/auth/login", json={ "email": "sarah.chen@gmail.com", "username": "sarahchen", "password": "YourPassword123!" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "user": { "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17", "email": "sarah.chen@gmail.com", "username": "sarahchen", "firstName": "Sarah", "lastName": "Chen", "role": [ "agent" ], "isActive": true, "teamId": "3c1e8f5a-7d2b-4a9c-b6e4-0f8d2c5a7e91", "teamName": "Chen Home Team", "brokerageId": "9e4b2d7f-1a6c-4e8b-a3d5-6c0f9b2e4d78", "corporationName": "Golden Empire Realty", "scopeLevel": "team", "verticals": [ "realtor" ], "managedTeams": [] }, "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ", "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI3ZTlkMmM0LTVmMWEtNGU4Yi05YzNkLTJhNmY4ZTBiNGQxNyJ9.Qm4x8pT2cVhLk9wYzR0aXNlY3VyZQ", "refreshToken": "9c2f8e4a1d6b3c5e7f0a2b4d6e8f1a3c5b7d9e0f2a4c6e8b1d3f5a7c9e0b2d4f", "expiresIn": "8h", "tokenType": "Bearer", "requiresTOTPSetup": false }, "message": "Login successful" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Password verified — data is one of four shapes (full login, TOTP required, SMS required, waitlisted) | | `401` | Invalid credentials (INVALID_CREDENTIALS) or disabled account (ACCOUNT_DISABLED) | | `403` | Account suspended (ACCOUNT_SUSPENDED) | | `423` | Account locked (ACCOUNT_LOCKED). The 5th consecutive failed attempt locks the account for 30 minutes; attempts made WHILE locked return this 423 with details.locked_until (the failure that triggers the lock itself returns 401). | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Refresh access token ```http POST /v1/auth/refresh ``` Exchanges the refresh token (httpOnly `refreshToken` cookie, or `refreshToken` in the request body for cookie-less clients) for a new access token. The refresh token is rotated on every call — the previous one is invalidated and the new one is set on the same httpOnly cookie. A device-fingerprint mismatch (different IP + user agent than the token was issued to) revokes the whole token family and returns 401 FINGERPRINT_MISMATCH. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `refreshToken` | string | No | Refresh token — only needed when the httpOnly cookie is unavailable (e.g. native clients) | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/auth/refresh" \ -H "Content-Type: application/json" \ -d '{ "refreshToken": "your-refreshToken" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "refreshToken": "your-refreshToken" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/auth/refresh", json={ "refreshToken": "your-refreshToken" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "accessToken": "example accessToken", "expiresIn": "8h", "refreshTokenExpiresIn": "30d" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Token refreshed (a rotated refreshToken cookie is also set) | | `401` | Missing, invalid, expired, or revoked refresh token (NO_REFRESH_TOKEN, INVALID_REFRESH_TOKEN, or FINGERPRINT_MISMATCH) | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Logout user ```http POST /v1/auth/logout ``` Revokes the current refresh token (if present) and clears the refreshToken cookie. Succeeds even without a token — the endpoint requires no authentication and always returns 200 for a well-formed request. ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/auth/logout" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/logout", { method: "POST", }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/auth/logout", ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "message": "Logged out successfully" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Logged out successfully (refreshToken cookie cleared) | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Verify token validity ```http GET /v1/auth/verify ``` Checks if the JWT access token is valid. Returns the same full profile payload as GET /auth/profile (both are served by the same handler). ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/auth/verify" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/verify", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/auth/verify", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "user": { "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17", "email": "sarah.chen@gmail.com", "role": [ "agent" ] } } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Token is valid — body matches GET /auth/profile | | `401` | Token missing, invalid, or expired | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get user profile ```http GET /v1/auth/profile ``` Returns authenticated user's profile information ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/auth/profile" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/profile", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/auth/profile", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "user": { "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17", "email": "sarah.chen@gmail.com", "username": "sarahchen", "firstName": "Sarah", "lastName": "Chen", "role": [ "agent" ], "isActive": true, "status": "active", "emailVerified": true, "licenseVerified": false, "lastLogin": "2026-07-15T14:32:10.000Z", "createdAt": "2026-07-15T14:32:10.000Z", "updatedAt": "2026-07-15T14:32:10.000Z", "timezone": "America/Los_Angeles", "teamId": "550e8400-e29b-41d4-a716-446655440000", "teamName": "example teamName", "brokerageId": "550e8400-e29b-41d4-a716-446655440000", "aiPlanTier": "free", "subscriptionTier": "free", "subscriptionStatus": "none", "hasStripeCustomer": false, "scopeLevel": "personal", "managedTeams": [ {} ] } } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Profile retrieved successfully (payload is nested under data.user) | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update user profile ```http PUT /v1/auth/profile ``` Updates authenticated user's profile information ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `firstName` | string | No | | | `lastName` | string | No | | | `currentPassword` | string | No | Required when changing the password · Format: password | | `newPassword` | string | No | New password — changing it revokes ALL refresh tokens (every session is logged out) · Format: password | | `homeCity` | string | No | | | `homeState` | string | No | | | `homeZip` | string | No | | | `homeLat` | number | No | | | `homeLng` | number | No | | | `licensedStates` | array of strings | No | | | `searchRadiusMiles` | integer | No | | | `timezone` | string | No | | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/auth/profile" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Sarah", "lastName": "Chen", "currentPassword": "your-currentPassword", "newPassword": "your-newPassword", "homeCity": "Bakersfield", "homeState": "CA", "homeZip": "93309", "homeLat": 35.3433, "homeLng": -119.0587, "licensedStates": [ "CA" ], "searchRadiusMiles": 25, "timezone": "America/Los_Angeles" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/profile", { method: "PUT", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "firstName": "Sarah", "lastName": "Chen", "currentPassword": "your-currentPassword", "newPassword": "your-newPassword", "homeCity": "Bakersfield", "homeState": "CA", "homeZip": "93309", "homeLat": 35.3433, "homeLng": -119.0587, "licensedStates": [ "CA" ], "searchRadiusMiles": 25, "timezone": "America/Los_Angeles" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/auth/profile", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "firstName": "Sarah", "lastName": "Chen", "currentPassword": "your-currentPassword", "newPassword": "your-newPassword", "homeCity": "Bakersfield", "homeState": "CA", "homeZip": "93309", "homeLat": 35.3433, "homeLng": -119.0587, "licensedStates": [ "CA" ], "searchRadiusMiles": 25, "timezone": "America/Los_Angeles" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "b7e9d2c4-5f1a-4e8b-9c3d-2a6f8e0b4d17", "email": "sarah.chen@gmail.com", "firstName": "Sarah", "lastName": "Chen", "role": [ "agent" ], "isActive": true, "home_city": "Bakersfield", "home_state": "CA", "home_zip": "93309", "home_lat": 35.3433, "home_lng": -119.0587, "licensed_states": [ "CA" ], "search_radius_miles": 25 }, "message": "Profile updated successfully" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Profile updated successfully | | `400` | Validation error (no fields to update, missing currentPassword, or incorrect current password) | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Logout from all devices ```http POST /v1/auth/logout-all ``` Invalidates all refresh tokens for the authenticated user, logging them out from all sessions ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/auth/logout-all" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/logout-all", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/auth/logout-all", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "message": "Logged out from all devices successfully" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Logged out from all devices successfully | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## List active sessions ```http GET /v1/auth/sessions ``` Returns list of active refresh tokens/sessions for authenticated user ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/auth/sessions" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/auth/sessions", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/auth/sessions", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "sessions": [ { "id": "3f8c1d7a-9e2b-4c6f-a1d8-7b4e2c9f5a30", "createdAt": "2026-06-28T17:03:22.481Z", "expiresAt": "2026-07-28T17:03:22.481Z", "ipAddress": "203.0.113.42", "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", "deviceInfo": {}, "isCurrent": true } ] } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Sessions retrieved successfully (array is nested under data.sessions) | | `401` | Unauthorized | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Billing API > REST endpoints for billing — request and response reference with examples. Subscription plans and checkout. The public-plans and public-checkout endpoints require no authentication; the rest operate on the authenticated account. Pricing on [www.actuallycare.com/pricing](https://www.actuallycare.com/pricing) comes from these endpoints. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## Get public plan pricing ```http GET /v1/billing/public-plans ``` Returns the current subscription plans with pricing in cents. This endpoint requires no authentication and is the source of truth for pricing shown on the marketing site. Responses are cacheable (5 minutes in browsers, 1 hour at the CDN edge). ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/billing/public-plans" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/public-plans"); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/billing/public-plans", ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "plans": [ { "key": "agent", "name": "ActuallyCare+ Agent", "priceMonthly": 10000, "priceYearly": 120000, "trialDays": 30, "seats": 1, "features": [ "example features" ] } ], "currency": "usd", "version": "example version" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Available plans | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Start checkout without an account ```http POST /v1/billing/public-checkout ``` Creates a Stripe Checkout session for a new customer who doesn't have an account yet. Only the agent plan is available through this endpoint; team and brokerage subscriptions require an account and the authenticated checkout endpoint. Returns the Stripe-hosted checkout URL to redirect the visitor to. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | Yes | Email address for the new customer and Stripe Checkout session · Format: email | | `planKey` | enum | No | Subscription plan tier (only agent is available without an account) · One of: `agent` · Default: `"agent"` | | `interval` | enum | No | Billing interval (defaults to monthly) · One of: `monthly`, `yearly` · Default: `"monthly"` | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/billing/public-checkout" \ -H "Content-Type: application/json" \ -d '{ "email": "agent@example.com" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/public-checkout", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ "email": "agent@example.com" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/billing/public-checkout", json={ "email": "agent@example.com" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "url": "example url", "sessionId": "example sessionId" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Checkout session created | | `400` | Invalid request data | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get plans with checkout identifiers ```http GET /v1/billing/plans ``` Returns the subscription plans for the in-app billing page. Same plans as the public endpoint, plus the identifiers needed to start an authenticated checkout. Prices here are in dollars. ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/billing/plans" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/plans", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/billing/plans", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "key": "agent", "name": "ActuallyCare+ Agent", "priceId": "price_1QxT4kGH2vSNn8LKcAgentMo", "priceIds": { "monthly": "price_1QxT4kGH2vSNn8LKcAgentMo", "yearly": "price_1QxT5bGH2vSNn8LKcAgentYr" }, "price": 100, "priceMonthly": 100, "priceYearly": 1000, "seats": 1, "escrowsPerMonth": -1, "includes": "base", "features": [ "1 agent seat", "Unlimited data", "Base AI included (~20 requests/session)" ] } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Available plans | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get current subscription status ```http GET /v1/billing/subscription ``` Returns the authenticated user's subscription state at each scope (personal, team, brokerage) plus which scope currently provides coverage. ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/billing/subscription" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/subscription", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/billing/subscription", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "personal": { "status": "active", "tier": "agent", "currentPeriodEnd": "2026-07-15T14:32:10.000Z", "canceledAt": "2026-07-15T14:32:10.000Z" }, "team": {}, "brokerage": {}, "effective_coverage": "brokerage" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Subscription status per scope | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Start checkout for a subscription ```http POST /v1/billing/checkout ``` Creates a Stripe Checkout session for the authenticated user at the requested scope. Valid scope and plan combinations are personal+agent, team+team, brokerage+team, and brokerage+broker. Eligibility depends on your role and how your brokerage pays for seats; ineligible requests return 400 with a specific reason. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `scope` | enum | Yes | Subscription scope the seat applies to (personal, team, or brokerage) · One of: `personal`, `team`, `brokerage` | | `planKey` | enum | Yes | Subscription plan tier to subscribe to · One of: `agent`, `team`, `broker` | | `interval` | enum | No | Billing interval (defaults to monthly) · One of: `monthly`, `yearly` · Default: `"monthly"` | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/billing/checkout" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "scope": "personal", "planKey": "agent" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/checkout", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "scope": "personal", "planKey": "agent" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/billing/checkout", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "scope": "personal", "planKey": "agent" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "url": "example url", "sessionId": "example sessionId" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Checkout session created | | `400` | Invalid scope/plan combination or eligibility check failed (the message explains why) | | `401` | Authentication required | | `404` | Team or brokerage not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Open the billing portal ```http POST /v1/billing/portal ``` Creates a Stripe customer portal session where the user can manage payment methods, view invoices, and change or cancel their subscription. Requires an existing billing account (returns 400 if the user has never subscribed). ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/billing/portal" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/billing/portal", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/billing/portal", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "url": "example url" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Portal session created | | `400` | No billing account found — subscribe to a plan first | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Clients API > REST endpoints for clients — request and response reference with examples. The people you represent — create, list, update, and remove client records. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List all clients ```http GET /v1/clients ``` Returns paginated list of client contacts. Default page size 25 (max 100). ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `client_type` | query | enum | No | One of: `buyer`, `seller`, `both` | | `status` | query | enum | No | One of: `active`, `inactive`, `closed` | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/clients" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/clients", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/clients", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "clients": [ { "id": "a3f8c1d2-6b4e-4a9f-8d27-95c0e3b1f684", "first_name": "Michael", "last_name": "Torres", "email": "michael.torres@gmail.com", "phone": "(661) 555-0142", "client_type": "buyer", "status": "active", "source": "Zillow", "budget_min": 350000, "budget_max": 475000 } ], "meta": { "page": 1, "limit": 25, "total": 42, "totalPages": 2, "hasMore": true } } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated list — records under data.clients, pagination under data.meta | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create new client ```http POST /v1/clients ``` ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | Yes | Client's first name | | `last_name` | string | Yes | Client's last name | | `email` | string | No | Client's email address | | `phone` | string | No | Client's phone number | | `client_type` | enum | Yes | Whether the client is a buyer, seller, or both · One of: `buyer`, `seller`, `both` | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/clients" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jordan", "last_name": "Lee", "client_type": "buyer" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/clients", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "first_name": "Jordan", "last_name": "Lee", "client_type": "buyer" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/clients", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "first_name": "Jordan", "last_name": "Lee", "client_type": "buyer" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "a3f8c1d2-6b4e-4a9f-8d27-95c0e3b1f684", "first_name": "Michael", "last_name": "Torres", "email": "michael.torres@gmail.com", "phone": "(661) 555-0142", "client_type": "buyer", "status": "active", "version": 1, "created_at": "2026-07-01T22:49:27.331Z", "updated_at": "2026-07-01T22:49:27.331Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Client created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get client by ID ```http GET /v1/clients/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/clients/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/clients/:id", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/clients/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "a3f8c1d2-6b4e-4a9f-8d27-95c0e3b1f684", "first_name": "Michael", "last_name": "Torres", "email": "michael.torres@gmail.com", "phone": "(661) 555-0142", "client_type": "buyer", "status": "active", "source": "Zillow", "budget_min": 350000, "budget_max": 475000, "pre_approved": true, "pre_approval_amount": 460000, "version": 2 } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Client found | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update client ```http PUT /v1/clients/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | No | Client's email address | | `phone` | string | No | Client's phone number | | `status` | string | No | Client status (active, inactive, closed) | | `version` | integer | No | Current record version for optimistic locking | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/clients/:id" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "agent@example.com", "phone": "+1 661 555 0123", "status": "your-status", "version": 3 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/clients/:id", { method: "PUT", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "email": "agent@example.com", "phone": "+1 661 555 0123", "status": "your-status", "version": 3 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/clients/:id", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "email": "agent@example.com", "phone": "+1 661 555 0123", "status": "your-status", "version": 3 }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "phone": "(555) 123-4567", "client_type": "buyer", "status": "active", "source": "Zillow", "budget_min": 400000, "budget_max": 650000, "preferred_locations": [ "example preferred locations" ], "property_preferences": { "bedrooms_min": 3, "bathrooms_min": 3, "square_feet_min": 1800, "property_types": [ "example property types" ] }, "pre_approved": true, "pre_approval_amount": 450000, "notes": "example notes", "tags": [ "example tags" ], "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Client updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete client ```http DELETE /v1/clients/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/clients/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/clients/:id", { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/clients/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": {}, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Client deleted | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Contacts API > REST endpoints for contacts — request and response reference with examples. Your wider address book — vendors, lenders, other agents, and everyone else in your sphere. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List contacts ```http GET /v1/contacts ``` Returns a paginated list of contacts visible to the authenticated user, with aggregate stats by status. Supports fuzzy search across name and email. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `search` | query | string | No | Fuzzy search across full name, first name, last name, and email | | `type` | query | string | No | Filter by contact type | | `archived` | query | boolean | No | Show archived contacts instead of active ones · Default: `false` | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/contacts" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/contacts", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "contacts": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } ], "stats": {}, "meta": { "total": 150, "page": 1, "limit": 20, "totalPages": 8, "hasMore": true } } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated contact list with stats | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create a contact ```http POST /v1/contacts ``` Adds a contact to the authenticated user's database. First name and contact type are required. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | Yes | Contact's first name | | `last_name` | string | No | Contact's last name | | `contact_type` | enum | Yes | Role this contact plays in transactions (e.g. buyer, seller, lender, inspector) · One of: `buyer`, `seller`, `agent`, `lender`, `title_officer`, `inspector`, `appraiser`, `contractor`, `client`, `vendor`, `other` | | `email` | string | No | Contact's email address · Format: email | | `phone` | string | No | Contact's phone number | | `company` | string | No | Company or organization the contact belongs to | | `license_number` | string | No | Professional license number (e.g. for agents, lenders, inspectors) | | `street_address` | string | No | Street address | | `city` | string | No | City name | | `state` | string | No | Two-letter state code | | `zip_code` | string | No | ZIP code | | `notes` | string | No | Free-form notes about the contact | | `tags` | array of strings | No | Free-form labels for grouping and filtering contacts | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/contacts" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jordan", "contact_type": "buyer" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "first_name": "Jordan", "contact_type": "buyer" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/contacts", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "first_name": "Jordan", "contact_type": "buyer" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Contact created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Search contacts ```http GET /v1/contacts/search ``` Searches the authenticated user's contacts by name, email, or role. Returns a flat array (not paginated) capped at the requested limit. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `name` | query | string | No | Case-insensitive substring match on full name | | `email` | query | string | No | Case-insensitive substring match on email | | `role` | query | string | No | Filter by assigned contact role name | | `limit` | query | integer | No | Default: `50` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/contacts/search" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/search", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/contacts/search", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } ], "count": 3 } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Matching contacts | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get a contact ```http GET /v1/contacts/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/contacts/:id" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/:id", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/contacts/:id", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | The contact | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update a contact ```http PUT /v1/contacts/{id} ``` Updates contact fields. All fields are optional; only the fields you send are changed. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | No | Contact's first name | | `last_name` | string | No | Contact's last name | | `email` | string | No | Contact's email address · Format: email | | `phone` | string | No | Contact's phone number | | `contact_type` | string | No | Role this contact plays in transactions (e.g. buyer, seller, lender, inspector) | | `contact_status` | string | No | Workflow status of the contact (free-form, e.g. active, lead, client, past_client) | | `company` | string | No | Company or organization the contact belongs to | | `street_address` | string | No | Street address | | `city` | string | No | City name | | `state` | string | No | Two-letter state code | | `zip_code` | string | No | ZIP code | | `notes` | string | No | Free-form notes about the contact | | `tags` | array of strings | No | Free-form labels for grouping and filtering contacts | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/contacts/:id" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jordan", "last_name": "Lee", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "your-contact-type", "contact_status": "your-contact-status", "company": "your-company", "street_address": "your-street-address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "your-notes", "tags": [ "your-tags" ] }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/:id", { method: "PUT", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "first_name": "Jordan", "last_name": "Lee", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "your-contact-type", "contact_status": "your-contact-status", "company": "your-company", "street_address": "your-street-address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "your-notes", "tags": [ "your-tags" ] }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/contacts/:id", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "first_name": "Jordan", "last_name": "Lee", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "your-contact-type", "contact_status": "your-contact-status", "company": "your-company", "street_address": "your-street-address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "your-notes", "tags": [ "your-tags" ] }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Contact updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete a contact ```http DELETE /v1/contacts/{id} ``` Soft-deletes a contact. Deleted contacts disappear from all lists and searches and cannot be restored; to hide a contact reversibly, use archive instead. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/contacts/:id" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/:id", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/contacts/:id", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Contact deleted (response echoes the deleted contact record) | | `401` | Authentication required | | `403` | Caller lacks permission to delete this contact | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Archive a contact ```http PATCH /v1/contacts/{id}/archive ``` Hides a contact from default lists. Archived contacts can be listed with `archived=true` and restored at any time. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X PATCH "https://api.actuallycare.com/v1/contacts/:id/archive" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/:id/archive", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.patch( "https://api.actuallycare.com/v1/contacts/:id/archive", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Contact archived (response is the updated contact record) | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Restore an archived contact ```http PATCH /v1/contacts/{id}/restore ``` Returns an archived contact to the active list. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X PATCH "https://api.actuallycare.com/v1/contacts/:id/restore" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/contacts/:id/restore", { method: "PATCH", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.patch( "https://api.actuallycare.com/v1/contacts/:id/restore", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "first_name": "Jordan", "last_name": "Lee", "full_name": "example full name", "email": "agent@example.com", "phone": "+1 661 555 0123", "contact_type": "buyer", "contact_status": "example contact status", "company": "example company", "license_number": "example license number", "street_address": "example street address", "city": "Bakersfield", "state": "CA", "zip_code": "93301", "notes": "example notes", "tags": [ "example tags" ], "is_archived": true, "archived_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Contact restored (response is the updated contact record) | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Escrows API > REST endpoints for escrows — request and response reference with examples. Transactions from contract to close — create, list, update, and manage escrows. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List escrows ```http GET /v1/escrows ``` Returns a paginated list of escrows visible to the authenticated user, with aggregate stats. Supports filtering by status, price range, closing-date range, and free-text search across property address and display ID. Note the effective default page size for this endpoint is 20 (a controller-level override of the platform-wide default of 25). ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | enum | No | Filter by escrow status (case-insensitive) · One of: `active`, `pending`, `closed`, `cancelled` | | `search` | query | string | No | Free-text search across property address and display ID | | `minPrice` | query | number | No | Minimum purchase price · Min: 0 | | `maxPrice` | query | number | No | Maximum purchase price · Min: 0 | | `closingDateStart` | query | string | No | Earliest closing date (ISO 8601) · Format: date | | `closingDateEnd` | query | string | No | Latest closing date (ISO 8601) · Format: date | | `archived` | query | boolean | No | Include archived escrows instead of active ones · Default: `false` | | `sort` | query | string | No | Sort column · Default: `"created_at"` | | `order` | query | enum | No | Sort direction · One of: `asc`, `desc` · Default: `"desc"` | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/escrows" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/escrows", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "escrows": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "property_address": "123 Main St, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "purchase_price": 500000, "escrow_status": "active", "acceptance_date": "2025-01-15", "closing_date": "2025-03-01", "buyers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "sellers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "listing_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "selling_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "escrow_company": "First American Title", "escrow_officer": "Jane Smith", "property_type": "single_family", "bedrooms": 3, "bathrooms": 2.5, "square_feet": 2400, "lot_size": 0.25, "year_built": 2015, "contingencies": [ { "type": "inspection", "due_date": "2026-07-15", "status": "pending" } ], "earnest_money": 10000, "commission_rate": 3, "notes": "example notes", "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z", "last_modified_by": "550e8400-e29b-41d4-a716-446655440000" } ], "stats": {}, "meta": { "page": 1, "limit": 20, "total": 150, "totalPages": 8, "hasMore": true } } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated escrow list with aggregate stats | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create an escrow ```http POST /v1/escrows ``` Opens a new escrow (transaction). Only the property address is required; financial, party, and vendor details can be added later. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `property_address` | string | Yes | Street address of the property in escrow | | `purchase_price` | number | No | Purchase price in USD | | `escrow_status` | enum | No | Initial escrow status (defaults to active) · One of: `active`, `pending`, `closed`, `cancelled` · Default: `"active"` | | `acceptance_date` | string | No | Offer acceptance date (ISO 8601) · Format: date | | `closing_date` | string | No | Expected closing date (ISO 8601) · Format: date | | `city` | string | No | City name | | `state` | string | No | State abbreviation (e.g. CA) | | `zip_code` | string | No | ZIP code | | `earnest_money_deposit` | number | No | Earnest money deposit amount in USD | | `commission_percentage` | number | No | Total commission rate as a percentage of purchase price | | `my_commission` | number | No | The authenticated agent's commission amount in USD | | `representation_type` | string | No | Which side the agent represents (e.g. buyer, seller, dual) | | `escrow_company` | string | No | Escrow company handling the transaction | | `title_company` | string | No | Title company name | | `buyers` | array of objects | No | Buyer parties to attach at creation | | `buyers[].name` | string | No | Buyer's full name | | `buyers[].email` | string | No | Buyer's email address | | `sellers` | array of objects | No | Seller parties to attach at creation | | `sellers[].name` | string | No | Seller's full name | | `sellers[].email` | string | No | Seller's email address | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/escrows" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "property_address": "123 Main St, Bakersfield, CA 93301" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "property_address": "123 Main St, Bakersfield, CA 93301" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/escrows", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "property_address": "123 Main St, Bakersfield, CA 93301" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "displayId": "ESC-2026-0001" } } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Escrow created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get an escrow ```http GET /v1/escrows/{id} ``` Returns a single escrow. The ID may be a UUID, a display ID (ESC-2026-0001), or the user-scoped numeric sequence. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Escrow UUID, display ID, or numeric sequence | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/escrows/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/escrows/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "property_address": "123 Main St, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "purchase_price": 500000, "escrow_status": "active", "acceptance_date": "2025-01-15", "closing_date": "2025-03-01", "buyers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "sellers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "listing_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "selling_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "escrow_company": "First American Title", "escrow_officer": "Jane Smith", "property_type": "single_family", "bedrooms": 3, "bathrooms": 2.5, "square_feet": 2400, "lot_size": 0.25, "year_built": 2015, "contingencies": [ { "type": "inspection", "due_date": "2026-07-15", "status": "pending" } ], "earnest_money": 10000, "commission_rate": 3, "notes": "example notes", "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z", "last_modified_by": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | The escrow | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update an escrow ```http PUT /v1/escrows/{id} ``` Updates escrow fields. Pass the current `version` to enable optimistic-locking; a stale version returns 409. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `purchase_price` | number | No | Purchase price in USD | | `closing_date` | string | No | Expected closing date (ISO 8601) · Format: date | | `escrow_status` | enum | No | Updated escrow status · One of: `active`, `pending`, `closed`, `cancelled` | | `version` | integer | No | Current record version for optimistic locking | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/escrows/:id" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "purchase_price": 450000, "closing_date": "2026-07-15", "escrow_status": "active", "version": 3 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id", { method: "PUT", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "purchase_price": 450000, "closing_date": "2026-07-15", "escrow_status": "active", "version": 3 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/escrows/:id", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "purchase_price": 450000, "closing_date": "2026-07-15", "escrow_status": "active", "version": 3 }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "property_address": "123 Main St, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "purchase_price": 500000, "escrow_status": "active", "acceptance_date": "2025-01-15", "closing_date": "2025-03-01", "buyers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "sellers": [ { "name": "John Doe", "email": "john@example.com", "phone": "(555) 123-4567" } ], "listing_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "selling_agent": { "name": "Alex Rivera", "email": "agent@example.com", "phone": "+1 661 555 0123", "license": "DRE #02209852", "brokerage": "Associated Real Estate" }, "escrow_company": "First American Title", "escrow_officer": "Jane Smith", "property_type": "single_family", "bedrooms": 3, "bathrooms": 2.5, "square_feet": 2400, "lot_size": 0.25, "year_built": 2015, "contingencies": [ { "type": "inspection", "due_date": "2026-07-15", "status": "pending" } ], "earnest_money": 10000, "commission_rate": 3, "notes": "example notes", "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z", "last_modified_by": "550e8400-e29b-41d4-a716-446655440000" }, "message": "Escrow updated successfully" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Escrow updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `409` | Version conflict (record changed since you read it) or business-rule violation | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete an escrow ```http DELETE /v1/escrows/{id} ``` Permanently deletes an escrow. The escrow must be archived first (PATCH /escrows/{id}/archive); deleting a non-archived escrow returns 400. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/escrows/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id", { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/escrows/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "message": "Escrow permanently deleted", "data": { "displayId": "ESC-2026-0847" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Escrow deleted | | `400` | Escrow is not archived — archive it before deleting | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Archive an escrow ```http PATCH /v1/escrows/{id}/archive ``` Moves an escrow to the archive. Archived escrows are hidden from default lists, can be restored at any time, and are the only escrows eligible for permanent deletion. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | | ### Example request ```bash title="cURL" curl -X PATCH "https://api.actuallycare.com/v1/escrows/:id/archive" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id/archive", { method: "PATCH", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.patch( "https://api.actuallycare.com/v1/escrows/:id/archive", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "message": "Escrow archived successfully", "data": { "displayId": "ESC-2026-0847" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Escrow archived | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Restore an archived escrow ```http PATCH /v1/escrows/{id}/restore ``` Returns an archived escrow to the active list. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | | ### Example request ```bash title="cURL" curl -X PATCH "https://api.actuallycare.com/v1/escrows/:id/restore" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id/restore", { method: "PATCH", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.patch( "https://api.actuallycare.com/v1/escrows/:id/restore", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "message": "Escrow restored successfully", "data": { "displayId": "ESC-2026-0847" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Escrow restored | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get an escrow's timeline ```http GET /v1/escrows/{id}/timeline ``` Returns the escrow's milestone timeline (deposits, contingencies, closing) with target dates, completion state, and a computed status for each milestone. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/escrows/:id/timeline" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/escrows/:id/timeline", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/escrows/:id/timeline", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "key": "example key", "label": "example label", "date": "2026-07-15T14:32:10.000Z", "completed": true, "completedDate": "2026-07-15T14:32:10.000Z", "status": "active", "sortOrder": 1 } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Timeline milestones in display order | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Leads API > REST endpoints for leads — request and response reference with examples. Lead capture and qualification, including converting a lead into a client. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List all leads ```http GET /v1/leads ``` Returns paginated list of prospective client leads. Default page size 25 (max 100). ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | enum | No | One of: `new`, `contacted`, `qualified`, `unqualified`, `converted`, `lost` | | `interest_level` | query | enum | No | One of: `hot`, `warm`, `cold` | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/leads" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/leads", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "leads": [ { "id": "f2b8d4a6-3c5e-4f7a-9b1d-8e0c6a4f2d9b", "name": "Rachel Kim", "email": "rachel.kim@outlook.com", "phone": "(661) 555-0198", "source": "Website Form", "status": "new", "lead_type": "buyer", "interest_level": "warm", "budget": 380000, "timeline": "3-6 months" } ], "meta": { "page": 1, "limit": 25, "total": 34, "totalPages": 2, "hasMore": true } }, "timestamp": "2026-07-01T22:52:30.664Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated list — records under data.leads, pagination under data.meta (the envelope also carries data.stats and data.cursors) | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create new lead ```http POST /v1/leads ``` ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | Lead's full name | | `email` | string | No | Lead's email address | | `phone` | string | No | Lead's phone number | | `source` | string | No | How the lead was acquired (e.g. Website Form, Zillow) | | `lead_type` | enum | No | Whether the lead is a buyer, seller, or both · One of: `buyer`, `seller`, `both` | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/leads" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Jordan Lee" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Jordan Lee" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/leads", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "name": "Jordan Lee" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "f2b8d4a6-3c5e-4f7a-9b1d-8e0c6a4f2d9b", "name": "Rachel Kim", "email": "rachel.kim@outlook.com", "phone": "(661) 555-0198", "source": "Website Form", "status": "new", "lead_type": "buyer", "interest_level": "warm", "version": 1, "created_at": "2026-07-01T22:53:05.882Z" }, "message": "Lead created successfully", "timestamp": "2026-07-01T22:53:05.890Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Lead created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get lead by ID ```http GET /v1/leads/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/leads/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads/:id", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/leads/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "f2b8d4a6-3c5e-4f7a-9b1d-8e0c6a4f2d9b", "name": "Rachel Kim", "email": "rachel.kim@outlook.com", "phone": "(661) 555-0198", "source": "Website Form", "status": "contacted", "lead_type": "buyer", "interest_level": "warm", "budget": 380000, "timeline": "3-6 months", "version": 2 }, "timestamp": "2026-07-01T22:53:41.126Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Lead found | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update lead ```http PUT /v1/leads/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `status` | string | No | Lead status (new, contacted, qualified, unqualified, converted, lost) | | `interest_level` | string | No | Lead's level of interest (hot, warm, cold) | | `version` | integer | No | Current record version for optimistic locking | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/leads/:id" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "your-status", "interest_level": "your-interest-level", "version": 3 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads/:id", { method: "PUT", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "status": "your-status", "interest_level": "your-interest-level", "version": 3 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/leads/:id", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "status": "your-status", "interest_level": "your-interest-level", "version": 3 }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "name": "Jane Smith", "email": "jane.smith@example.com", "phone": "(555) 987-6543", "source": "Website Form", "status": "new", "lead_type": "buyer", "interest_level": "warm", "budget": 450000, "timeline": "3-6 months", "message": "example message", "notes": "example notes", "last_contact_date": "2026-07-15T14:32:10.000Z", "next_follow_up": "2026-07-15T14:32:10.000Z", "converted_to_client_id": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" }, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Lead updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete lead ```http DELETE /v1/leads/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/leads/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads/:id", { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/leads/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": {}, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Lead deleted | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Convert lead to client ```http POST /v1/leads/{id}/convert ``` Converts a qualified lead into a full client record (transactional). Creates or reuses a contact, creates the client, and marks the lead converted. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `clientType` | enum | No | Client type for the new client record · One of: `buyer`, `seller`, `both` · Default: `"buyer"` | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/leads/:id/convert" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "clientType": "buyer" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/leads/:id/convert", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "clientType": "buyer" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/leads/:id/convert", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "clientType": "buyer" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "client": { "id": "c9d2e4f6-1a3b-4c5d-8e7f-0b2d4f6a8c1e", "contact_id": "7e5a3c1b-9d8f-4e2a-b6c4-1f0e8d7a5b3c", "client_type": "buyer", "status": "active", "price_range_min": 342000, "price_range_max": 418000 }, "lead": { "id": "f2b8d4a6-3c5e-4f7a-9b1d-8e0c6a4f2d9b", "lead_status": "converted", "client_id": "c9d2e4f6-1a3b-4c5d-8e7f-0b2d4f6a8c1e" }, "primaryContactId": "7e5a3c1b-9d8f-4e2a-b6c4-1f0e8d7a5b3c", "additionalContactsCount": 0 }, "message": "Lead successfully converted to client", "timestamp": "2026-07-01T22:54:29.773Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Lead converted successfully | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Listings API > REST endpoints for listings — request and response reference with examples. Property inventory — create, list, update, and remove listings. Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List all property listings ```http GET /v1/listings ``` Returns paginated list of property listings with optional filtering. Default page size 25 (max 100). ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `status` | query | enum | No | One of: `active`, `pending`, `sold`, `withdrawn`, `expired` | | `minPrice` | query | number | No | | | `maxPrice` | query | number | No | | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/listings" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/listings", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "listings": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "mls_number": "ML81992437", "address": "456 Oak Ave, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "list_price": 425000, "status": "active", "property_type": "single_family", "bedrooms": 4, "bathrooms": 3, "square_feet": 2800 } ], "meta": { "page": 1, "limit": 25, "total": 87, "totalPages": 4, "hasMore": true } }, "timestamp": "2026-07-01T22:45:03.120Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Paginated list — records under data.listings, pagination under data.meta (the envelope also carries data.stats aggregates) | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create new listing ```http POST /v1/listings ``` ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | Street address of the property | | `list_price` | number | Yes | Listing price in USD | | `property_type` | string | No | Property type (e.g. single_family, condo, townhouse, multi_family, land, commercial) | | `bedrooms` | integer | No | Number of bedrooms | | `bathrooms` | number | No | Number of bathrooms | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/listings" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "address": "123 Main St, Bakersfield, CA 93301", "list_price": 450000 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings", { method: "POST", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "address": "123 Main St, Bakersfield, CA 93301", "list_price": 450000 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/listings", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "address": "123 Main St, Bakersfield, CA 93301", "list_price": 450000 }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "8c5e2a9f-4d7b-4e3a-b1c8-6f0d9e2a5c74", "address": "1847 Cerro Vista Dr, Bakersfield, CA 93306", "list_price": 389000, "status": "active", "property_type": "single_family", "bedrooms": 3, "bathrooms": 2, "version": 1, "created_at": "2026-07-01T22:46:18.402Z", "updated_at": "2026-07-01T22:46:18.402Z" }, "timestamp": "2026-07-01T22:46:18.410Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Listing created | | `400` | Invalid request data | | `401` | Authentication required | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get listing by ID ```http GET /v1/listings/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/listings/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings/:id", { headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/listings/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "mls_number": "ML81992437", "address": "456 Oak Ave, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "list_price": 425000, "status": "active", "property_type": "single_family", "bedrooms": 4, "bathrooms": 3, "square_feet": 2800, "year_built": 2010, "version": 3 }, "timestamp": "2026-07-01T22:47:00.115Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Listing found | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update listing ```http PUT /v1/listings/{id} ``` ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `list_price` | number | No | Listing price in USD | | `status` | string | No | Listing status (active, pending, sold, withdrawn, expired) | | `version` | integer | No | Current record version for optimistic locking | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/listings/:id" \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "list_price": 450000, "status": "your-status", "version": 3 }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings/:id", { method: "PUT", headers: { "X-API-Key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ "list_price": 450000, "status": "your-status", "version": 3 }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/listings/:id", headers={"X-API-Key": "YOUR_API_KEY"}, json={ "list_price": 450000, "status": "your-status", "version": 3 }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "550e8400-e29b-41d4-a716-446655440000", "mls_number": "ML123456", "address": "456 Oak Ave, Tehachapi, CA 93561", "city": "Tehachapi", "state": "CA", "zip_code": "93561", "list_price": 425000, "status": "active", "property_type": "single_family", "bedrooms": 4, "bathrooms": 3, "square_feet": 2800, "lot_size": 0.5, "year_built": 2010, "description": "example description", "listing_date": "2026-07-15", "expiration_date": "2026-07-15", "photos": [ "example photos" ], "virtual_tour_url": "example virtual tour url", "showing_instructions": "example showing instructions", "days_on_market": 1, "price_per_sqft": 1800, "version": 1, "created_at": "2026-07-15T14:32:10.000Z", "updated_at": "2026-07-15T14:32:10.000Z" }, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Listing updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Delete listing ```http DELETE /v1/listings/{id} ``` Permanently deletes a listing (hard delete — only allowed after the listing has been archived) ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Unique identifier (UUID) · Format: uuid | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/listings/:id" \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/listings/:id", { method: "DELETE", headers: { "X-API-Key": "YOUR_API_KEY", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/listings/:id", headers={"X-API-Key": "YOUR_API_KEY"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": {}, "timestamp": "2026-07-15T14:32:10.000Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Listing deleted | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Webhooks API > REST endpoints for webhooks — request and response reference with examples. Manage webhook subscriptions and browse the self-documenting event catalog. Concepts and signature verification live in [Webhooks](/concepts/webhooks); the hands-on walkthrough is [Set up webhooks](/guides/webhook-setup). Base URL: `https://api.actuallycare.com/v1` · Errors use the [standard envelope](/api/reference#error-responses) — see [Errors](/api/errors). ## List webhook subscriptions ```http GET /v1/webhooks ``` Returns the webhook subscriptions registered for your account. Requires the broker or system admin role. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/webhooks" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/webhooks", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "id": "wh_abc123def456", "url": "example url", "events": [ "escrow.created" ], "secret": "example secret", "active": true, "last_triggered": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Webhook subscriptions | | `401` | Authentication required | | `403` | Requires broker or system admin role | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Create a webhook subscription ```http POST /v1/webhooks ``` Registers an endpoint to receive event notifications. Each delivery is an HTTPS POST with the JSON payload, an `X-Webhook-Event` header naming the event, an `X-Webhook-Timestamp` header, and an `X-Webhook-Signature` header containing `sha256=` followed by the hex-encoded HMAC-SHA256 of the payload computed with your secret. Each event gets 4 delivery attempts total: the initial delivery plus retries after 1, 5, and 15 minutes. Only HTTP 5xx, 408, and 429 responses (and network failures) are retried — any other 4xx response is logged and NOT retried. ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | Yes | Endpoint to deliver events to · Format: uri | | `events` | array of strings | Yes | Event names to subscribe to (see GET /webhooks/events for the catalog) | | `secret` | string | Yes | Shared secret used to sign every delivery | ### Example request ```bash title="cURL" curl -X POST "https://api.actuallycare.com/v1/webhooks" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "url": "your-url", "events": [ "escrow.created" ], "secret": "your-secret" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks", { method: "POST", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "your-url", "events": [ "escrow.created" ], "secret": "your-secret" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.post( "https://api.actuallycare.com/v1/webhooks", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "url": "your-url", "events": [ "escrow.created" ], "secret": "your-secret" }, ) data = resp.json() ``` ### Example response (201) ```json { "success": true, "data": { "id": "wh_abc123def456", "url": "example url", "events": [ "escrow.created" ], "secret": "example secret", "active": true, "last_triggered": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `201` | Webhook created | | `400` | Invalid request data | | `401` | Authentication required | | `403` | Requires broker or system admin role | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## List available webhook events ```http GET /v1/webhooks/events ``` Returns the catalog of events you can subscribe to, including each event's category, description, and the field names included in its payload. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `category` | query | string | No | Filter to a single category (e.g. escrow, lead, email) | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/webhooks/events" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks/events", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/webhooks/events", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "event": "escrow.created", "category": "escrow", "description": "example description", "payload": [ "example payload" ] } ], "meta": { "total": 150, "categories": [ "escrow" ] } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Event catalog | | `401` | Authentication required | | `403` | Requires broker or system admin role | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## List webhook event categories ```http GET /v1/webhooks/events/categories ``` Returns every event category with the number of events it contains. ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/webhooks/events/categories" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks/events/categories", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/webhooks/events/categories", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "category": "escrow", "event_count": 3 } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Categories with event counts | | `401` | Authentication required | | `403` | Requires broker or system admin role | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Update a webhook subscription ```http PUT /v1/webhooks/{id} ``` Updates a subscription's URL, events, secret, or active flag. Only the fields you send are changed. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Webhook ID | ### Request body | Field | Type | Required | Description | | --- | --- | --- | --- | | `url` | string | No | Endpoint that receives event deliveries · Format: uri | | `events` | array of strings | No | Subscribed event names (entity.action format) | | `active` | boolean | No | Whether the subscription is enabled to receive deliveries | | `secret` | string | No | Shared secret used to sign deliveries (minimum 10 characters) | ### Example request ```bash title="cURL" curl -X PUT "https://api.actuallycare.com/v1/webhooks/:id" \ -H "Authorization: Bearer YOUR_JWT" \ -H "Content-Type: application/json" \ -d '{ "url": "your-url", "events": [ "your-events" ], "active": true, "secret": "your-secret" }' ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id", { method: "PUT", headers: { "Authorization": "Bearer YOUR_JWT", "Content-Type": "application/json", }, body: JSON.stringify({ "url": "your-url", "events": [ "your-events" ], "active": true, "secret": "your-secret" }), }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.put( "https://api.actuallycare.com/v1/webhooks/:id", headers={"Authorization": "Bearer YOUR_JWT"}, json={ "url": "your-url", "events": [ "your-events" ], "active": true, "secret": "your-secret" }, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "wh_abc123def456", "url": "example url", "events": [ "escrow.created" ], "secret": "example secret", "active": true, "last_triggered": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" } } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Webhook updated | | `400` | Invalid request data | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Deactivate a webhook subscription ```http DELETE /v1/webhooks/{id} ``` Deactivates a subscription so it stops receiving deliveries. The subscription record is kept and can be re-enabled by setting `active` back to true. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Webhook ID | ### Example request ```bash title="cURL" curl -X DELETE "https://api.actuallycare.com/v1/webhooks/:id" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.delete( "https://api.actuallycare.com/v1/webhooks/:id", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": { "id": "wh_abc123def456", "url": "example url", "events": [ "escrow.created" ], "secret": "example secret", "active": true, "last_triggered": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" }, "timestamp": "2026-07-01T22:51:14.902Z" } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Webhook deactivated (response is the updated subscription with active=false) | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | ## Get webhook delivery logs ```http GET /v1/webhooks/{id}/logs ``` Returns recent delivery attempts for a subscription, including the payload sent, the HTTP status your endpoint returned, and whether delivery succeeded. ### Parameters | Parameter | In | Type | Required | Description | | --- | --- | --- | --- | --- | | `id` | path | string | Yes | Webhook ID | | `page` | query | integer | No | Page number for pagination · Default: `1` · Min: 1 | | `limit` | query | integer | No | Number of items per page (default 25; GET /escrows overrides this to 20 at the controller level) · Default: `25` · Max: 100 · Min: 1 | ### Example request ```bash title="cURL" curl "https://api.actuallycare.com/v1/webhooks/:id/logs" \ -H "Authorization: Bearer YOUR_JWT" ``` ```javascript title="JavaScript" const res = await fetch("https://api.actuallycare.com/v1/webhooks/:id/logs", { headers: { "Authorization": "Bearer YOUR_JWT", }, }); const data = await res.json(); ``` ```python title="Python" import requests resp = requests.get( "https://api.actuallycare.com/v1/webhooks/:id/logs", headers={"Authorization": "Bearer YOUR_JWT"}, ) data = resp.json() ``` ### Example response (200) ```json { "success": true, "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "webhook_id": "550e8400-e29b-41d4-a716-446655440000", "event": "escrow.created", "payload": {}, "response_status": 200, "delivered_at": "2026-07-15T14:32:10.000Z", "created_at": "2026-07-15T14:32:10.000Z" } ] } ``` ### Responses | Status | Meaning | | --- | --- | | `200` | Delivery log entries, most recent first | | `401` | Authentication required | | `404` | Resource not found | | `429` | Rate limit exceeded — too many requests in the current window. Wait for the window to reset (see Retry-After) before retrying. | | `500` | Internal server error | --- # Versioning & deprecation > How the ActuallyCare REST API is versioned, what counts as a breaking change, and how deprecations are communicated. The API is path-versioned. There is currently **one version: `/v1`** — every endpoint lives under `https://api.actuallycare.com/v1`. ## What we change without notice (non-breaking) These can happen at any time and your integration must tolerate them: - New endpoints, and new **optional** request parameters - New fields in response objects (parse what you need; ignore unknown fields) - New enum values in fields that are documented as open-ended (e.g. new `status` values on new features) - New webhook event types (subscribe explicitly; ignore types you don't handle) - Documentation-only changes to the OpenAPI spec (descriptions, examples) ## What counts as breaking Anything that would make a working integration stop working: - Removing or renaming an endpoint, request parameter, or response field - Changing a field's type or meaning - Tightening validation so previously-valid requests fail - Removing an enum value or changing error `code` strings **Breaking changes ship under a new version path** (`/v2`), not in place. `/v1` keeps working while any published version is supported. ## How deprecations are communicated No `/v1` endpoint is deprecated today. When a deprecation is scheduled it will be announced in the [changelog](https://www.actuallycare.com/changelog), marked `deprecated: true` in the [OpenAPI spec](https://api.actuallycare.com/v1/openapi.json), and given an explicit sunset date in both places — with a migration path documented before the clock starts. ## MCP tools MCP tools follow the same philosophy at the tool level: tool inputs only gain optional parameters in place, and renamed tools keep their old name callable as an alias (see the `aliasOf` field in [tools.json](https://docs.actuallycare.com/tools.json)). New tools appear without notice — enumerate them via `tools/list` or the discovery tools rather than hardcoding the tool set. --- # Blog > Notes on follow-up, AI, and building a CRM that actually cares. Occasional notes on follow-up, AI, and building a CRM that actually cares. - [Introducing ActuallyCare - Client Relationship Management for Real Estate](/blog/introducing-actuallycare) — January 15, 2025 — The public launch: why traditional CRMs fail real estate agents, and how Care Status plus AI changes that. - [The Real Estate Agent's Biggest Missed Opportunity](/blog/why-follow-up-matters) — January 10, 2025 — Most referrals come from people you already know, so why does all the budget go to chasing strangers? - [Using AI to Manage Your Client Relationships](/blog/mcp-integration-guide) — January 5, 2025 — What it looks like to run your client database by talking to Claude through MCP. --- # Introducing ActuallyCare - Client Relationship Management for Real Estate > Announcing ActuallyCare, a platform built to help real estate professionals never lose track of a client again. Today I'm excited to publicly launch ActuallyCare, a platform built to help real estate professionals never lose track of a client again. ## The Problem Every real estate agent knows the pain: you meet someone at an open house, add them to your contacts, and then... life happens. Weeks pass. By the time you remember to follow up, they've already found another agent. Traditional CRMs are either too complex or too generic. They weren't built for how real estate actually works—the long sales cycles, the importance of personal relationships, and the need to stay top-of-mind without being annoying. ## What We Built ActuallyCare is designed around one simple idea: **make it impossible to forget about your clients**. The core of the system is what we call "Care Status." Every contact in your database has a status that shows whether they need attention. When someone needs care, you'll know. When you've reached out, mark it done. It's that simple. But here's where it gets interesting: ActuallyCare integrates directly with AI assistants through MCP (Model Context Protocol). This means you can use natural language to manage your entire client database: - "Who haven't I talked to in 30 days?" - "Schedule a check-in with all my buyers next week" - "Add a note that Sarah mentioned she's interested in the Riverside area" ## What's Next We're just getting started. Over the coming months, we'll be adding: - Automated care reminders - Integration with popular email platforms - Team collaboration features - Analytics and insights If you're a real estate professional who wants to build stronger client relationships, I'd love for you to give ActuallyCare a try. [Get started at www.actuallycare.com →](https://www.actuallycare.com) --- # Using AI to Manage Your Client Relationships > How connecting Claude to ActuallyCare through MCP changes the way you interact with your client database. One of the most powerful features of ActuallyCare is its integration with AI assistants through the Model Context Protocol (MCP). In this post, I'll show you how this changes the way you interact with your client database. ## What is MCP? MCP (Model Context Protocol) is an open standard that lets AI assistants like Claude connect directly to external tools and data sources. Instead of copying and pasting information back and forth, you can just talk to your AI assistant and it handles the rest. ## Real Examples Here's what this looks like in practice. Instead of clicking through menus and filling out forms, you can just say: **Adding a new contact:** > "Add John Smith as a new lead. His email is john@example.com, phone is 555-0123. He's interested in buying a home in the $400-500k range in the downtown area." **Checking who needs follow-up:** > "Show me all clients I haven't contacted in the last 2 weeks who are actively looking." **Adding notes after a conversation:** > "I just spoke with Maria Garcia. Add a note that she's now pre-approved for $600k and wants to start looking at homes next month. Also schedule a follow-up for February 1st." **Getting a daily briefing:** > "Give me a summary of what I need to do today—any appointments, overdue follow-ups, and clients who need care." ## Setting It Up Getting started takes about 5 minutes, and it all happens in your browser: 1. In Claude's settings, add ActuallyCare as a custom connector 2. Sign in to your ActuallyCare account and approve access 3. Start talking to your data For step-by-step instructions, check out our [quickstart guide](/mcp/quickstart). ## Why This Matters The goal isn't to replace human connection with AI—it's the opposite. By reducing the friction of managing your database, you free up more time and mental energy for what actually matters: building real relationships with your clients. When adding a note after a call takes 5 seconds instead of 2 minutes, you're more likely to do it. When checking who needs follow-up is a simple question instead of a multi-step process, you'll check more often. The AI handles the administrative work so you can focus on the human work. ## Try It Out If you're already an ActuallyCare user, connect Claude with the [quickstart guide](/mcp/quickstart) and start asking. If you're new, [sign up at www.actuallycare.com](https://www.actuallycare.com) and see how AI-powered CRM feels. --- # The Real Estate Agent's Biggest Missed Opportunity > Most agents chase new leads while their sphere of influence — the source of most referrals — goes quiet. The math says that's backwards. According to NAR, 64% of sellers found their agent through a referral or used an agent they'd worked with before. Yet most agents spend the majority of their marketing budget chasing new leads. Something doesn't add up. ## The Math on Follow-Up Let's run some numbers. The average real estate agent has somewhere between 200-500 people in their sphere of influence—past clients, friends, family, and acquaintances who know they're in real estate. If each of those people knows roughly 250 other people (the commonly cited average network size), that's access to 50,000-125,000 potential referrals. But here's the catch: people can only refer you if they remember you exist. ## Why Agents Drop the Ball I've talked to hundreds of agents about their follow-up habits. The answers are almost always the same: 1. **"I mean to, but I get busy"** - Transactions are demanding. When you're in the middle of a deal, everything else falls away. 2. **"I don't know what to say"** - After the initial "checking in" email, what do you talk about? It feels awkward. 3. **"My CRM is a mess"** - Contacts are scattered across phones, email, and various apps. Finding who to call is harder than making the call. 4. **"I don't want to be annoying"** - There's a real fear of coming across as salesy or desperate. ## The Solution Isn't More Reminders Most CRMs try to solve this with automated drip campaigns and reminder notifications. But here's what I've learned: more notifications don't help if the underlying system is broken. What agents actually need is: - **Clarity** - At a glance, who needs attention right now? - **Context** - What did we last talk about? What's going on in their life? - **Confidence** - A system that makes follow-up feel natural, not forced This is exactly what we built ActuallyCare to do. Instead of drowning you in reminders, we surface the people who need attention and give you the context to have meaningful conversations. ## Start Small If you're reading this and feeling guilty about your follow-up habits, here's my advice: start with five people. Not fifty. Five. Pick five people from your past clients or sphere who you haven't talked to in over 90 days. Reach out this week. Not with a sales pitch—just to check in and see how they're doing. That's it. Five people. One week. You might be surprised what happens. --- # Concepts > How ActuallyCare's integration surface fits together: API vs MCP, authentication, the data model, and webhooks. The thinking-clearly pages. Read these when you want to understand how the pieces fit together, before (or instead of) memorizing endpoints. ## [API vs MCP](/concepts/api-vs-mcp) Two ways to integrate — when to use the REST API, when to use the MCP server, and how to combine them. ## [Authentication model](/concepts/authentication) The three credential types — OAuth for Claude users, API keys for servers, JWTs for user sessions — and when each applies. ## [Data model](/concepts/data-model) The entities behind your CRM — contacts vs clients vs leads, transactions, the vendor pipeline, property management, and how visibility follows the team/brokerage hierarchy. ## [Webhooks](/concepts/webhooks) Event-driven integration: the self-documenting event catalog, payload signatures, and delivery semantics. --- # API vs MCP > Two ways to integrate - when to use which, and how to combine them. ActuallyCare gives you two ways to integrate: a REST API and an MCP (Model Context Protocol) server. They sit in front of the same database — the difference is who's doing the talking. The REST API is for code you write. MCP is for an AI assistant like Claude. ## Which one do you need? | If you're building... | Reach for | |---|---| | A custom UI, dashboard, or internal tool | REST API | | A data sync with another system (MLS feed, accounting, email marketing) | REST API | | An event-driven integration that reacts to changes | REST API plus [webhooks](/concepts/webhooks) | | An AI assistant that can actually work your CRM | MCP | | A daily "ask Claude about my pipeline" workflow | MCP | | All of the above | Both — this is common | ## At a glance | Aspect | REST API | MCP | |---|---|---| | Interface | HTTP requests you write | Natural language via Claude | | Best for | Apps, scripts, syncs | Conversational work and AI analysis | | Authentication | API key (`X-API-Key`) or JWT | OAuth 2.1, handled by Claude | | Response format | JSON envelope | Claude reads tool results and answers in plain English | | Surface | Small and focused | Much larger | Both authentication models are covered in depth in [the authentication model](/concepts/authentication). ## When to use the REST API Choose REST when a program — not a person — is the consumer. You control the exact request, you parse the exact response, and you can schedule, retry, and log however you like. ```bash curl "https://api.actuallycare.com/v1/clients?page=1&limit=20" \ -H "X-API-Key: YOUR_API_KEY" ``` Every response uses the same envelope: ```json { "success": true, "data": { "clients": [], "meta": { "page": 1, "limit": 20, "total": 0, "totalPages": 0, "hasMore": false } }, "timestamp": "2026-06-11T17:32:08.123Z" } ``` Here `data` is an object keyed by the resource — the page of clients lives at `data.clients`, with pagination metadata at `data.meta`. See [pagination](/api/pagination) and [errors](/api/errors) for the details, and the [endpoint reference](/api/reference) for every documented operation. Good REST use cases: - **Custom applications** — internal dashboards, reporting tools, mobile front ends - **Automation** — scheduled scripts, Zapier-style workflows, bulk imports (see [the bulk import guide](/guides/bulk-import)) - **Syncing with other systems** — push leads in from your website, push closings out to accounting - **Reacting to events** — pair the API with [webhooks](/concepts/webhooks) instead of polling ## When to use MCP Choose MCP when you want to talk to your CRM instead of program against it. Once your CRM is connected to Claude (see the [MCP quickstart](/mcp/quickstart)), you ask in plain English: > "Show me all escrows closing this month with their current status." Claude picks the right tool — in this case [`escrows_list`](/tools/escrows/escrows_list) with date filters — runs it against your data, and presents the results conversationally. Tool names follow an entity_verb convention: `escrows_list`, `clients_create`, `leads_convert`, `appointments_today`. Good MCP use cases: - **Quick lookups** — "What's the status of 123 Main St?" - **Natural updates** — "Mark John Smith as contacted" - **Analysis** — "Which of my deals look at risk of falling through?" - **Multi-step questions** — "Find clients who bought in 2024 and haven't heard from me in 90 days" To keep conversations fast, Claude sees the most-used tools up front and finds the rest when it needs them — you don't have to do anything to enable a tool. ## Two surfaces, one database The two surfaces are deliberately different sizes. The documented REST surface is focused: ten resource groups covering the core CRM entities (contacts, clients, leads, escrows, listings, appointments) plus auth, API keys, webhooks, and billing. The full list lives in the [endpoint reference](/api/reference). The MCP surface is much larger. It covers escrows, showings, open houses, financial calculators, the vendor pipeline, property management, reporting, and more — browse the full catalog at [the tool reference](/tools), where the live tool counts are rendered from the same registry the server uses. That means some things you can ask Claude to do don't have a documented REST equivalent yet. If you need an operation that only exists as an MCP tool, MCP is the answer — including from your own code, via a [custom MCP client](/mcp/custom-apps). ## Using both together Many teams run both, and that's the recommended setup for power users: - **MCP for daily work** — quick queries, updates, and analysis while chatting with Claude - **REST for automation** — webhook handlers, scheduled jobs, and integrations that run without a human in the loop A typical pattern: a webhook fires when a lead is created, your server enriches it via the REST API, and the agent reviews and works the lead through Claude the next morning. ## Data consistency Both interfaces read and write the same underlying records. A client Claude creates is immediately visible to your REST integration, and vice versa. There's no sync delay and no separate data store to reconcile. ## Rate limits The two surfaces are limited independently: - **REST API** — 500 requests per 15 minutes per IP across `/v1` (auth endpoints are tighter: 30 **failed** attempts per 15 minutes, plus a 50-total-attempts cap on `POST /v1/auth/login` — that second cap is what the `RateLimit-Limit` header on login responses reports — with `/v1/auth/refresh` on its own 30-per-minute limit) - **MCP** — 120 requests per minute and 30 tool calls per minute per user When you hit a limit you'll get a 429 — back off and retry. Full details, including the response headers to watch, are on the [rate limits page](/api/rate-limits). --- # Authentication model > How OAuth, API keys, and JWTs fit together across the platform. ActuallyCare has one backend and three credential types. Which one you need depends entirely on who — or what — is making the call. This page explains the model; for copy-paste REST details see [API authentication](/api/authentication), and for the plain-English version written for agents see [permissions and safety](/agents/permissions-and-safety). ## Who uses what | You are... | You use... | Why | |---|---|---| | An agent connecting Claude to your CRM | OAuth 2.1 | Claude handles the whole flow — you just log in and approve once | | A developer writing a server-side integration or script | API key | One long-lived secret, no login flow, no token refresh logic | | A developer building an app where your users sign in | JWT | Short-lived tokens tied to a real user session, with refresh rotation | ## OAuth 2.1 — for Claude users When you add ActuallyCare as a connector in Claude, Claude starts an OAuth 2.1 flow with PKCE (Proof Key for Code Exchange — a standard protection against intercepted authorization codes). You never see a token. What you do see is the consent page at `app.actuallycare.com/mcp/authorize`, titled "Grant Claude access to ActuallyCare". It lists exactly what you're approving: 1. Read your CRM data — escrows, listings, clients, leads, and calendar 2. Create and update records when you ask 3. Draft emails and texts for your approval — nothing sends without your okay 4. Archive or delete records, with confirmation If you're not already logged in to ActuallyCare, you go through the normal login first. After approval: - **Access tokens last 24 hours.** - **Refresh tokens rotate automatically**, so you stay connected without re-approving. To disconnect, remove the connector in Claude's own settings (Settings, then Connectors). That's the revocation path — there is no separate ActuallyCare page for revoking Claude's access. Custom MCP clients can also use OAuth 2.1 via dynamic client registration — see [building custom apps](/mcp/custom-apps). ## API keys — for servers and scripts API keys are the simplest credential for code that runs unattended. Send the key in the `X-API-Key` header (the header `API-Key` also works): ```bash curl "https://api.actuallycare.com/v1/listings" \ -H "X-API-Key: YOUR_API_KEY" ``` Key facts: - Keys are **64-character hex strings** — there's no prefix to look for. - The full key is shown **once**, at creation. It's stored hashed; afterwards the UI only shows the first 8 and last 4 characters. If you lose a key, create a new one. - A key acts as the user who created it, so requests inherit that user's role and visibility. - Scopes **grant** a key its access — they don't narrow it. A key created without scopes has no access at all and gets `403` on every endpoint. Grant `{"all":["read","write"]}` for broad access, or per-resource grants like `{"clients":["read","write"]}`, and adjust later with `PATCH /v1/api-keys/:id/scopes`. (Scopes are an API-key concept — JWT users aren't scope-checked.) Create keys in the app under Settings, in the API Keys tab (`app.actuallycare.com/settings?tab=api`). Every user can create their own keys — each key acts as you, with your role's visibility. You can also create keys via the API itself: `POST /v1/api-keys` with a `name`, a `scopes` object, and an optional `expiresInDays` between 1 and 365. If you omit `expiresInDays`, the key never expires — there's no implicit default, so set an expiry explicitly. Standard hygiene applies: keep keys in environment variables, never commit them to source control, never ship them in client-side code, and rotate them periodically. ## JWTs — for user-session apps If you're building an app where each user logs in with their own ActuallyCare account, use JWT auth. Log in: ```bash curl -X POST "https://api.actuallycare.com/v1/auth/login" \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com", "password": "YOUR_PASSWORD"}' ``` The response follows the standard envelope: ```json { "success": true, "data": { "user": {}, "accessToken": "...", "refreshToken": "...", "expiresIn": "15m", "tokenType": "Bearer" }, "timestamp": "2026-06-11T17:32:08.123Z" } ``` Then send the access token on every request: ```text Authorization: Bearer YOUR_ACCESS_TOKEN ``` What to know: - **Access-token lifetime is role-based**, ranging from 15 minutes to 8 hours. The `expiresIn` field is a **duration string** like `"15m"` or `"8h"` — not seconds — so parse the string or decode the JWT's `exp` claim rather than assuming a number. - **Refresh with `POST /v1/auth/refresh`.** The refresh token rotates on every use, with a 30-day sliding window and a 90-day absolute cap. After that, the user logs in again. - **The same JWT also works against the MCP server**, so a user-session app can call MCP tools on the user's behalf. ## One backend, three front doors The credentials map onto the two surfaces like this: | Surface | Accepts | |---|---| | REST API (`api.actuallycare.com/v1`) | API key, JWT | | MCP server (`mcp.actuallycare.com/mcp`) | OAuth 2.1, API key, JWT | Whichever door you come through, you're acting as a specific user with a specific role — authentication decides who you are, and the role decides what you can see and change. How roles shape data visibility is covered in [the data model](/concepts/data-model). --- # Data model > The entities behind your CRM and how they relate. ActuallyCare's data model follows the shape of a real estate business: people you know, deals in flight, properties you market, and the calendar that ties it together — plus newer verticals for vendor relationships and property management. This page is the conceptual map. For the exact fields and parameters of any operation, see the [tool reference](/tools) and the [endpoint reference](/api/reference). ## The core six The six entities you'll touch every day: | Entity | What it is | |---|---| | Contacts | Every person in your database | | Clients | People you represent | | Leads | Potential business in your pipeline | | Escrows | Active transactions, open to close | | Listings | Properties you're marketing | | Appointments | Your calendar | ### Contacts vs clients vs leads This distinction confuses almost everyone at first, so here it is plainly: - A **contact** is a *person*. Everyone in your database — past clients, leads, other agents, your lender, your mom — is a contact. It's the universal address book. - A **lead** is *potential business*. A lead represents someone moving through your pipeline who might transact with you. Leads get qualified, nurtured, and eventually [converted](/tools/leads/leads_convert) — or archived. - A **client** is an *active representation relationship*. When someone actually works with you — buying, selling, or both — they're a client. The key insight: lead and client aren't different people, they're different *relationships with the same person*. A typical journey is contact first, then lead, then client. The contact record is the person; the lead and client records describe what that person is to your business right now. ### Escrows, listings, and appointments - **Escrows** are transactions. An escrow opens when a deal goes under contract and tracks everything through closing. - **Listings** are inventory — properties you're marketing. A listing and an escrow often describe the same property at different stages: you list it, then it goes into escrow. - **Appointments** are calendar entries — showings you're driving to, listing presentations, closings, anything with a time attached. ## Around the transaction An escrow at the center of a deal accumulates satellite records: - **Documents** — files attached to the transaction. Documents can be run through AI risk analysis to surface red flags (see [`documents_get_risks`](/tools/documents/documents_get_risks)). - **Showings** — individual property showings, including buyer feedback. - **Open houses** — events with their own schedule and visitor logging. - **Deadlines and checklists** — contingency deadlines, the escrow checklist, and deadline alerts so nothing slips during the contract period. ## The vendor pipeline "Vendor" here means the service businesses around a transaction — lenders, escrow officers, inspectors, contractors, insurance agents, and other professionals you exchange business with. The vendor pipeline tracks those relationships the way the core pipeline tracks buyers and sellers: - **Prospects** — vendors you're evaluating but don't work with yet - **Partners** — vendors you actively work with - **Referrals** — business passed between you and a partner, in either direction - **Deals** — revenue opportunities that come out of those relationships - **Engagements** — logged interactions with partners, so the relationship history is on the record ## Property management For agents and brokerages doing property management, a dedicated set of entities covers the rental side: - **Rental applications** — applicants for a rental; an approved application can convert directly into a lease - **Leases** — the agreements themselves, with expiration tracking - **Tenancies** — the ongoing tenant relationship: rent collection, inspections, delinquency - **PM consultations** — intake conversations with property owners considering your management services; a consultation can convert into a tenancy ## Care status Care status is ActuallyCare's measure of follow-up health: is this relationship being actively cared for, or is it going cold? It changes as you log activity, and it gives "who needs attention?" a concrete answer instead of a gut feeling. Care status changes also fire `care_status.*` [webhook events](/concepts/webhooks), so you can build follow-up automation on top of it. ## Custom pipelines Beyond the built-in pipelines, ActuallyCare supports config-driven pipelines: pipeline types and stages are configuration, not code. Records move through stages, activity gets logged along the way, and you get pipeline-level reporting — which lets a team model a workflow the built-ins don't cover. See the [pipeline tools](/tools/pipelines). ## Entity links Sometimes two records are related in a way no built-in relationship captures — a partner who's the lender on a specific escrow, a document that matters to two different clients. **Entity links** are lightweight cross-references between any two records, in any combination. See the [entity link tools](/tools/entity-links). ## How your org shapes what you see Every user belongs to a team, and every team belongs to a brokerage: ```text Brokerage └─ Team └─ Agent (user) ├─ Contacts ── the person record │ ├─ as a Lead ──(convert)──► as a Client │ └─ Appointments ├─ Listings ◄─┐ ├─ Escrows ───┘ often the same property, two stages │ ├─ Documents (with AI risk analysis) │ ├─ Deadlines and checklists │ ├─ Showings │ └─ Open houses ├─ Vendor pipeline: Prospects ► Partners ► Referrals · Deals · Engagements └─ Property mgmt: Applications ► Leases ► Tenancies · PM consultations ``` Data visibility follows four **visibility scopes** that map onto the hierarchy: **standard** (your own records), **team** (across your team), **brokerage** (across the brokerage), and **admin** (platform administration). Roles are how users land in a scope — the classic real estate roles map directly (agent → standard, team_lead → team, broker → brokerage, system_admin → admin), and other verticals add their own roles (lender, property_manager, escrow_officer, and more) that slot into the same four scopes. Whatever credential you connect with (Claude, an API key, a JWT), you act as a specific user, so this hierarchy applies everywhere — see [the authentication model](/concepts/authentication). ## IDs and lifecycle Every record's primary key is a **UUID**. There are no special ID formats to parse — treat IDs as opaque strings. Core entities share a soft-delete lifecycle: 1. **Archive** — the record leaves normal lists but nothing is lost. Reversible. 2. **Restore** — bring an archived record back, exactly as it was. 3. **Delete** — destructive and permanent. Deletes are role-gated and require confirmation. Bulk versions of archive, restore, and delete exist for the core entities — see the [bulk operations tools](/tools/bulk-operations). When in doubt, archive: it's the safe default, and you can always restore. --- # Webhooks > Event-driven integration: catalog, signatures, retries. Webhooks turn your integration from pull to push. Instead of polling the API on a schedule and diffing results, you register a URL once and ActuallyCare sends an HTTP POST to it the moment something happens. This page covers the concepts — event catalog, payload, signatures, delivery. For a step-by-step walkthrough, see the [webhook setup guide](/guides/webhook-setup). ## Webhooks or polling? | Situation | Better fit | |---|---| | React to changes as they happen (new lead, escrow update) | Webhooks | | Periodic full sync or reporting snapshot | Polling the REST API | | You can't host a public HTTPS endpoint | Polling | | Low latency matters (lead routing, notifications) | Webhooks | The usual pattern is both: webhooks for the real-time signal, with an occasional polled reconciliation pass as a safety net. ## The event catalog The catalog is self-documenting. Ask the API what events exist (all `/v1/webhooks` endpoints take a JWT bearer token — not an API key — and need a broker or system-admin role, see [Managing webhooks](#managing-webhooks)): ```bash curl "https://api.actuallycare.com/v1/webhooks/events" \ -H "Authorization: Bearer YOUR_JWT" ``` The response uses the standard envelope: `data` lists every event type with its category, description, and the fields its delivery payload carries, and `meta` totals it up. Trimmed to two entries: ```json { "success": true, "data": [ { "event": "escrow.created", "category": "escrow", "description": "An escrow/transaction was created", "payload": ["id", "status", "property_address", "escrow_number"] }, { "event": "lead.created", "category": "lead", "description": "A new lead was created", "payload": ["id", "name", "email", "phone", "source", "status"] } ], "meta": { "total": 150, "categories": ["appointment", "care_status", "client", "..."] } } ``` There are about 150 event types across roughly 27 categories — `escrow.*`, `lead.*`, `client.*`, `listing.*`, `appointment.*`, `care_status.*`, and more. A few representative examples: | Event | Fires when | |---|---| | `escrow.created` | A new escrow is opened | | `escrow.updated` | An escrow's details change | | `lead.created` | A new lead enters the pipeline | | `lead.converted` | A lead becomes a client | | `client.created` | A new client relationship is created | | `listing.updated` | A listing's details change | | `appointment.cancelled` | An appointment is cancelled | | `care_status.changed` | A relationship's follow-up health changes | This table is illustrative — **the catalog endpoint is the source of truth.** `GET /v1/webhooks/events/categories` returns the category list if you want to browse by group. When you create a webhook you subscribe it to specific events, or to all of them. ## The payload envelope Every delivery is a POST with a JSON body in the same envelope: ```json { "event": "escrow.created", "team_id": "3f8a1c2e-9d4b-4f6a-8e2d-1b7c5a9e0f3d", "timestamp": "2026-06-11T17:32:08.123Z", "data": { "id": "9c4e7b1a-2f8d-4e3c-a6b5-0d1f2e3a4b5c" } } ``` | Field | Meaning | |---|---| | `event` | The event type that fired | | `team_id` | The team the event belongs to (webhooks are team-scoped) | | `timestamp` | When the event occurred | | `data` | The affected record — its shape depends on the event type | ## Verifying signatures Every delivery is signed so you can confirm it really came from ActuallyCare. Three headers arrive with each request: | Header | Contents | |---|---| | `X-Webhook-Signature` | `sha256=` followed by the HMAC-SHA256 hex digest of the JSON body, computed with your webhook secret (e.g. `sha256=3f5a…`). | | `X-Webhook-Event` | The event type, same as `event` in the body | | `X-Webhook-Timestamp` | When the delivery was sent | Compute the same HMAC over the raw request body and compare with a timing-safe comparison — never a plain string equality: ```javascript const crypto = require('crypto'); const express = require('express'); const app = express(); function verifySignature(rawBody, signatureHeader, secret) { const expectedHex = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); // Header format: "sha256=" const receivedHex = (signatureHeader || '').replace(/^sha256=/, ''); const expected = Buffer.from(expectedHex, 'hex'); const received = Buffer.from(receivedHex, 'hex'); return ( expected.length === received.length && crypto.timingSafeEqual(expected, received) ); } app.post( '/webhooks/actuallycare', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.get('X-Webhook-Signature'); if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('invalid signature'); } // Acknowledge fast, process async res.status(200).send('ok'); const payload = JSON.parse(req.body.toString('utf8')); // handle payload.event, payload.data ... } ); ``` Two details that bite people: verify against the **raw** body (a re-serialized JSON object may not match byte-for-byte), and remember to strip the `sha256=` prefix from the header before comparing digests. Your webhook secret is set when you create the webhook (minimum 10 characters). ## Delivery and retries - ActuallyCare waits up to **30 seconds** for your endpoint to respond. Return a 2xx quickly and do real work asynchronously. - **Failed deliveries are retried** at 1 minute, 5 minutes, and 15 minutes after the initial attempt (up to 4 attempts total), then given up. A delivery counts as failed on a network error or on a `5xx`, `408`, or `429` response; other `4xx` responses are treated as your endpoint rejecting the event and are **not** retried. - Retries survive server restarts, and deactivating a webhook cancels its pending retries. - Delivery is **at-least-once**: your endpoint can see the same event more than once, so process idempotently. The delivery logs — visible in the app, and available via `GET /v1/webhooks/:id/logs` — show every attempt. Deduplicate on the signature header value — a retried delivery carries the same signature as the original, and unlike `event` + `timestamp` it can't collide when two records fire the same event in the same instant. ## Managing webhooks Webhooks are **team-scoped** and managed by **broker** and **system-admin** roles. Two ways to manage them: - **In the app** — Settings, Webhooks tab (`app.actuallycare.com/settings?tab=webhooks`): create endpoints, choose events, and review delivery logs. - **Via REST** — `GET`/`POST /v1/webhooks` to list and create, `PUT`/`DELETE /v1/webhooks/:id` to update and remove, `GET /v1/webhooks/:id/logs` for delivery history. Ready to wire one up end to end? Follow the [webhook setup guide](/guides/webhook-setup). --- # Guides > Hands-on tutorials: build an integration, set up webhooks, and import your data. Step-by-step tutorials for building on ActuallyCare. Each one is complete and copy-pasteable — start at the top and you'll end with something working. ## Build a custom integration Go from an API key to a working two-way sync — an API client, paginated reads, creating records, and a webhook receiver. [Build a custom integration →](/guides/build-custom-integration) ## Set up webhooks Discover the event catalog, register a webhook, verify signatures, and process deliveries reliably. [Set up webhooks →](/guides/webhook-setup) ## Bulk import your data Bring contacts, clients, and leads over from your old CRM with a rate-limit-aware import script and a field-mapping checklist. [Bulk import your data →](/guides/bulk-import) ## Connecting Claude? That guide lives on the agent path: [Connect Claude in 5 minutes](/mcp/quickstart). Building your own MCP app instead? See [Build a custom MCP app](/mcp/custom-apps). --- # Build a custom integration > An end-to-end tutorial: API key, client module, paginated lead sync, and a verified webhook receiver. This tutorial builds a complete, production-shaped integration with a concrete goal: **a nightly sync that pulls new leads out of ActuallyCare into your own system, and pushes status updates back**. Along the way you'll add a webhook receiver so urgent events arrive in real time instead of waiting for the next sync run. Everything here is copy-pasteable. You'll need: - An ActuallyCare account with API access - Node.js 18 or newer (which has `fetch` built in), or Python 3.9+ if you prefer the Python examples - About 30 minutes ## Step 1: Get an API key All requests in this tutorial authenticate with an API key sent in the `X-API-Key` header. 1. Open [app.actuallycare.com](https://app.actuallycare.com) and go to **Settings → API Keys** (direct link: `https://app.actuallycare.com/settings?tab=api`). 2. Create a new key. Give it a name like `nightly-lead-sync`, an expiration, and the scopes it needs — scopes are what grant a key access, and a key with no scopes gets `403` on every endpoint. For this tutorial, `{"leads":["read","write"]}` covers reading and updating leads (or `{"all":["read","write"]}` for broad access). 3. **Copy the key immediately.** It's a 64-character hex string, shown once at creation. After that, ActuallyCare stores only a hash and the UI shows just the first 8 and last 4 characters. Every user can create their own keys — each key acts as you, with your role's visibility. Store the key in an environment variable, never in code: ```bash export ACTUALLYCARE_API_KEY="your-64-character-key" ``` Sanity-check it with curl: ```bash curl "https://api.actuallycare.com/v1/leads?limit=1" \ -H "X-API-Key: $ACTUALLYCARE_API_KEY" ``` Every response uses the same envelope. On list endpoints `data` is an object keyed by the resource, with pagination metadata at `data.meta`. Success looks like this (record fields trimmed): ```json { "success": true, "data": { "leads": [ { "id": "a1b2c3d4-..." } ], "meta": { "page": 1, "limit": 1, "total": 132, "totalPages": 132, "hasMore": true } }, "timestamp": "2026-06-11T17:42:11.000Z" } ``` And errors look like this (an illustrative example): ```json { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "name is required." } } ``` Rate-limited `429`s use the same envelope (`error.code` of `RATE_LIMIT_EXCEEDED`) and add a `Retry-After` header — branch on the HTTP status or the code, and honor the header. For more on keys, JWTs, and scopes, see [Authentication](/api/authentication). ## Step 2: Write a small API client Wrap the envelope handling, error handling, and retry logic once, in one module, so the rest of your integration never thinks about it. ### Node.js ```javascript // actuallycare.js const BASE_URL = 'https://api.actuallycare.com/v1'; const API_KEY = process.env.ACTUALLYCARE_API_KEY; class ApiError extends Error { constructor(status, code, message) { super(`${code}: ${message}`); this.status = status; this.code = code; } } const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function request(method, path, { params, body } = {}) { const url = new URL(BASE_URL + path); for (const [key, value] of Object.entries(params || {})) { url.searchParams.set(key, value); } // Retry on 429 and 5xx with exponential backoff: 1s, 2s, 4s. for (let attempt = 0; ; attempt++) { const res = await fetch(url, { method, headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); if ((res.status === 429 || res.status >= 500) && attempt < 3) { await sleep(1000 * 2 ** attempt); continue; } if (!res.ok) { // Parse error bodies defensively — proxies can return non-JSON. let err = {}; try { err = (await res.json()).error || {}; } catch { /* non-JSON body */ } throw new ApiError(res.status, err.code || `HTTP_${res.status}`, err.message || 'Request failed'); } const payload = await res.json(); return payload.data; // the envelope's data object, e.g. { leads: [...], meta: {...} } } } module.exports = { ApiError, get: (path, params) => request('GET', path, { params }), post: (path, body) => request('POST', path, { body }), put: (path, body) => request('PUT', path, { body }), }; ``` ### Python ```python # actuallycare.py import os import time import requests BASE_URL = "https://api.actuallycare.com/v1" API_KEY = os.environ["ACTUALLYCARE_API_KEY"] class ApiError(Exception): def __init__(self, status, code, message): super().__init__(f"{code}: {message}") self.status = status self.code = code def request(method, path, params=None, body=None): headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"} # Retry on 429 and 5xx with exponential backoff: 1s, 2s, 4s. for attempt in range(4): res = requests.request( method, BASE_URL + path, headers=headers, params=params, json=body, timeout=30, ) if (res.status_code == 429 or res.status_code >= 500) and attempt < 3: time.sleep(2 ** attempt) continue if not res.ok: # Parse error bodies defensively — proxies can return non-JSON. try: err = res.json().get("error", {}) except ValueError: err = {} raise ApiError(res.status_code, err.get("code", f"HTTP_{res.status_code}"), err.get("message", res.text.strip() or "Request failed")) # The envelope's data object, e.g. {"leads": [...], "meta": {...}} return res.json()["data"] def get(path, **params): return request("GET", path, params=params) def post(path, body): return request("POST", path, body=body) def put(path, body): return request("PUT", path, body=body) ``` Two things worth noting: - The wrapper retries on 429 (rate limited) and 5xx automatically. The limit is 500 requests per 15 minutes per IP — see [Rate limits](/api/rate-limits) for the response headers that tell you where you stand. - Errors surface as a typed exception carrying the HTTP status and, when the body is the JSON envelope, its `error.code` (a `429` carries `RATE_LIMIT_EXCEEDED`). Branch on `err.status` or `err.code`, never on messages; the defensive parse falls back to `HTTP_` if a proxy returns a non-JSON body. ## Step 3: Fetch leads with pagination List endpoints take `page` and `limit` query parameters (`limit` defaults to 25, max 100). The records come back under `data`, keyed by the resource — `data.leads` here — with pagination metadata at `data.meta`. Loop pages until `meta.hasMore` goes `false`: ```javascript // fetch-leads.js const api = require('./actuallycare'); async function getAllLeads() { const leads = []; const limit = 100; for (let page = 1; ; page++) { const data = await api.get('/leads', { page, limit }); leads.push(...data.leads); console.log(`Page ${page}: ${data.leads.length} leads (${leads.length} of ${data.meta.total})`); if (!data.meta.hasMore) break; } return leads; } module.exports = { getAllLeads }; ``` The `meta` object also carries `total` and `totalPages` — see [Pagination](/api/pagination) for the exact shape, and [the leads reference](/api/reference/leads) for everything the endpoint accepts and returns. ## Step 4: Create and update records, with real error handling The nightly sync has two write paths: creating a lead that originated in your system, and pushing a status change back to ActuallyCare. The key habit: **check before you create**. The API doesn't know your external system's IDs, so dedupe on something stable like email before inserting — otherwise a re-run of a failed sync creates duplicates. ```javascript // sync.js const fs = require('fs'); const api = require('./actuallycare'); const { getAllLeads } = require('./fetch-leads'); const STATE_FILE = './synced-lead-ids.json'; function loadState() { try { return new Set(JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))); } catch { return new Set(); } } async function nightlySync() { const synced = loadState(); const leads = await getAllLeads(); // 1. Pull: hand any lead we haven't seen before to your own system. for (const lead of leads) { if (synced.has(lead.id)) continue; await sendToYourSystem(lead); // your code here synced.add(lead.id); } fs.writeFileSync(STATE_FILE, JSON.stringify([...synced])); // 2. Push: send status changes from your system back to ActuallyCare. for (const change of await getPendingStatusChanges()) { // your code here try { await api.put(`/leads/${change.leadId}`, { status: change.newStatus }); console.log(`Updated lead ${change.leadId} -> ${change.newStatus}`); } catch (err) { if (err.status === 429) { // The client already retried; if it still failed, stop and resume next run. console.error('Rate limited after retries, resuming next run.'); break; } // Validation and not-found errors: log the code and keep going. console.error(`Lead ${change.leadId} failed: ${err.code} - ${err.message}`); } } } nightlySync().catch((err) => { console.error('Sync failed:', err); process.exit(1); }); ``` Field names in request bodies (like `status` above) are illustrative — the generated [endpoint reference](/api/reference) is the source of truth for each resource's exact request and response shapes. Same goes for creating records: a `POST /v1/leads` with the fields from [the leads reference](/api/reference/leads) returns the created record (including its UUID `id`) inside `data`. Run the script from cron, a scheduled CI job, or whatever scheduler you already have. Once a night is plenty — the webhook receiver in the next step covers the in-between. ## Step 5: Receive webhooks in real time Polling nightly means a hot lead could sit for hours. Webhooks fix that: ActuallyCare POSTs to your endpoint when events happen, signed so you can prove they're genuine. Register a webhook first — the [webhook setup guide](/guides/webhook-setup) covers discovery, registration, and the event catalog. Here's the receiving end in Express: ```javascript // webhook-server.js const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.ACTUALLYCARE_WEBHOOK_SECRET; // Capture the raw body — the signature is computed over the exact bytes sent, // so verify against those, not a re-serialized object. app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; }, })); function verifySignature(req) { const received = req.get('X-Webhook-Signature') || ''; const expected = crypto .createHmac('sha256', SECRET) .update(req.rawBody) .digest('hex'); // raw hex digest — no "sha256=" prefix const a = Buffer.from(received, 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b); } app.post('/webhooks/actuallycare', (req, res) => { if (!verifySignature(req)) { return res.status(401).send('invalid signature'); } // Respond immediately — deliveries time out after 30 seconds, // and a timeout counts as a failure that triggers retries. res.status(200).send('ok'); // Process asynchronously, after the response is sent. setImmediate(() => handleEvent(req.body)); }); function handleEvent(payload) { // Payload envelope: { "event": "...", "team_id": "...", "timestamp": "...", "data": {...} } // Use exact event names from GET /v1/webhooks/events. if (payload.event.startsWith('lead.')) { console.log(`Lead event ${payload.event} at ${payload.timestamp}`); // e.g. push payload.data into your system right away } } app.listen(3000, () => console.log('Listening on :3000')); ``` Three rules keep webhook handling boring (in the good way): 1. **Verify every delivery.** HMAC-SHA256 hex of the raw JSON body using your webhook secret, compared timing-safe against `X-Webhook-Signature`. 2. **Return 200 fast, work later.** Deliveries time out after 30 seconds. Failures (network errors, or `5xx`/`408`/`429` responses) are retried at 1, 5, and 15 minutes, then given up — other `4xx` responses are not retried, so don't reject events you just haven't handled yet. 3. **Be idempotent.** Delivery is at-least-once — the same event can arrive more than once. Dedupe on the `event` plus `timestamp` pair before acting. More background on the event catalog and delivery semantics lives in [Webhooks](/concepts/webhooks). ## Step 6: Production checklist Before you point this at real data: - **Keys in environment variables.** Never commit `ACTUALLYCARE_API_KEY` or `ACTUALLYCARE_WEBHOOK_SECRET`. Use your platform's secret store in production. - **Backoff on 429 and 5xx.** The client module above retries with exponential backoff; keep that behavior if you replace it. Budget your request rate against the 500-per-15-minutes limit — see [Rate limits](/api/rate-limits). - **Idempotency everywhere.** Check before you create (Step 4), dedupe webhook deliveries (Step 5), and persist sync state so a crashed run resumes instead of re-importing. - **Log requests and error codes.** When something fails at 2 a.m., the envelope's `error.code` plus the request you sent is usually all you need to diagnose it. The [errors page](/api/errors) explains the envelope. - **Set key expirations and rotate.** Expiration is optional at creation, and a key without one never expires — so set one explicitly. Treat a leaked key like a leaked password: delete it in Settings → API Keys and issue a new one. - **Watch your webhook logs.** Delivery logs are visible in the app under Settings → Webhooks, and via `GET /v1/webhooks/:id/logs`. From here, the [endpoint reference](/api/reference) documents every resource this tutorial touched, and the [API vs MCP](/concepts/api-vs-mcp) page explains when a Claude-driven MCP integration is a better fit than REST. --- # Bulk import your data > Bring clients and leads in from your old CRM with a rate-limit-aware import script. Moving from another CRM? This guide walks you through exporting your data as CSV, mapping it onto ActuallyCare's entities, and importing it through the REST API with a script that respects rate limits and never creates duplicates. **Honest scope note:** this is an API-based import — you'll run a small script from your terminal. If you'd rather have help migrating, or you want a hands-off import, reach out to support through [www.actuallycare.com](https://www.actuallycare.com) instead. ## Step 1: Export from your old CRM Every mainstream CRM can export contacts as CSV. Look for **Export**, **Download**, or **Backup** in its settings, and export everything to one or more CSV files with a header row. While you're in there, clean the data — it's far easier before import than after: - Remove blank rows and obvious junk records - Standardize phone formats - Fix or remove invalid email addresses - Merge duplicate rows (same person, two entries) ## Step 2: Decide what each record is ActuallyCare distinguishes between three people-shaped entities — the [data model](/concepts/data-model) page covers them in depth: | Entity | Who belongs there | Import via | |---|---|---| | **Leads** | People in your pipeline you don't represent yet — inquiries, open-house sign-ins, referrals to follow up | `POST /v1/leads` | | **Clients** | People you represent or have represented — active buyers and sellers, and your past-client base | `POST /v1/clients` | | **Contacts** | Everyone else in your sphere (vendors, other agents, lenders) | `POST /v1/contacts` — requires `first_name` and `contact_type` | Most old-CRM exports mix all three together. The simplest approach: add a column to your CSV (or split it into two files) marking each row as a lead or a client. Past clients are clients — they're the relationships your follow-up automations care about. Vendors, escrow officers, and other agents belong in **Contacts** (`POST /v1/contacts` with `first_name` and a `contact_type`). The documented surface also covers Escrows if you're bringing over open transactions — the [endpoint reference](/api/reference) is the current list of everything importable. ## Step 3: Map your fields CRMs name the same fields differently. Common mappings: | Your old CRM's column | Maps to | |---|---| | First Name, FirstName, Given Name | first name | | Last Name, LastName, Surname | last name | | Email, Email Address, Primary Email | email | | Phone, Mobile, Cell, Primary Phone | phone | | Lead Source, Source, Origin | lead source | | Notes, Comments, Description | notes | The authoritative field names and which are required come from the generated reference — check [clients](/api/reference/clients) and [leads](/api/reference/leads) before you write your mapping, and adjust the `mapRow` function in Step 4 to match. Don't guess — check the reference for the exact spelling of every field you map. One difference that bites people: clients take `first_name` and `last_name`, but **leads take a single `name` field** — the `mapRow` function in Step 4 handles both. If your data has a source or tag-like field, stamp every imported row with a marker such as `import-2026-06`. Months later, being able to find exactly what this import created (or archive it, if something went wrong) is worth a lot. ## Step 4: The import script The script below: 1. Reads your CSV 2. Fetches your **existing** records first and indexes them by email, so re-running the script never creates duplicates 3. Creates one record every 2 seconds — about 450 requests per 15 minutes, safely under the [500-per-15-minutes rate limit](/api/rate-limits) 4. Logs failures to a file so you can fix and re-run just those rows It reuses the `actuallycare.js` API client module from the [custom integration tutorial](/guides/build-custom-integration) — copy that file into the same folder first. Then: ```bash mkdir crm-import && cd crm-import npm init -y npm install csv-parse # copy actuallycare.js (from the custom integration tutorial) into this folder export ACTUALLYCARE_API_KEY="your-64-character-key" ``` ```javascript // import.js // Usage: node import.js [--limit N] const fs = require('fs'); const { parse } = require('csv-parse/sync'); const api = require('./actuallycare'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const PACE_MS = 2000; // 1 request per 2s ≈ 450 per 15 min, under the 500 limit // Adjust to your CSV's column names and the fields documented at // /api/reference/clients and /api/reference/leads. // The shapes differ: clients take first_name/last_name, // while leads take a single name field. function mapRow(row, resource) { const email = row.email?.toLowerCase().trim() || undefined; const phone = row.phone?.trim() || undefined; if (resource === 'leads') { const name = [row.first_name, row.last_name] .map((s) => s?.trim()) .filter(Boolean) .join(' '); return { name, email, phone }; } return { first_name: row.first_name?.trim(), last_name: row.last_name?.trim(), email, phone, }; } // Page through existing records once and index by email, // so we can dedupe before creating anything. // List responses put records at data. with pagination at data.meta. async function fetchExistingEmails(resource) { const emails = new Set(); const limit = 100; for (let page = 1; ; page++) { const data = await api.get(`/${resource}`, { page, limit }); for (const record of data[resource]) { if (record.email) emails.add(record.email.toLowerCase().trim()); } if (!data.meta.hasMore) break; } return emails; } async function run() { const args = process.argv.slice(2); const [csvPath, resource] = args; const limitIdx = args.indexOf('--limit'); const maxRows = limitIdx === -1 ? Infinity : Number(args[limitIdx + 1]); if (!csvPath || !['clients', 'leads'].includes(resource)) { console.error('Usage: node import.js [--limit N]'); process.exit(1); } const rows = parse(fs.readFileSync(csvPath), { columns: true, skip_empty_lines: true, }).slice(0, maxRows); console.log(`Checking existing ${resource} for duplicates...`); const existing = await fetchExistingEmails(resource); console.log(`${existing.size} existing emails indexed. Importing ${rows.length} rows...`); const results = { created: 0, skipped: 0, failed: [] }; for (const [i, row] of rows.entries()) { const record = mapRow(row, resource); if (record.email && existing.has(record.email)) { results.skipped++; console.log(`[${i + 1}/${rows.length}] skip (exists): ${record.email}`); continue; } try { await api.post(`/${resource}`, record); results.created++; if (record.email) existing.add(record.email); console.log(`[${i + 1}/${rows.length}] created: ${record.email || record.name || record.last_name}`); } catch (err) { results.failed.push({ row, code: err.code, message: err.message }); console.error(`[${i + 1}/${rows.length}] FAILED: ${err.code} - ${err.message}`); } await sleep(PACE_MS); // stay under the rate limit } console.log(`\nDone. Created ${results.created}, skipped ${results.skipped}, failed ${results.failed.length}.`); if (results.failed.length > 0) { fs.writeFileSync('failed-rows.json', JSON.stringify(results.failed, null, 2)); console.log('Failed rows written to failed-rows.json — fix and re-run.'); } } run().catch((err) => { console.error('Import aborted:', err); process.exit(1); }); ``` A few design notes: - **Dedupe before create.** The API doesn't know your old CRM's record IDs, so email is the stable key. Skipping existing emails makes the script safe to re-run after a crash or a partial import. - **Pacing matters.** At one create per 2 seconds, a 1,000-row import takes about 35 minutes. That's intentional — bursting through the rate limit gets you `429` responses ([rate limits](/api/rate-limits)) and a forced wait anyway. Let it run in the background. - **Failures are data.** A failed row usually means a missing required field or a malformed value. The `error.code` in `failed-rows.json` tells you which. ## Step 5: Dry run with 5 rows Never run the whole file first. Import five rows: ```bash node import.js my-export.csv clients --limit 5 ``` Then open [app.actuallycare.com](https://app.actuallycare.com) and check those five records in the Clients section. Verify: - Names landed in the right fields (not reversed, no stray whitespace) - Emails and phones look right - Your source/tag marker is present, if you mapped one If something's off, fix `mapRow`, archive the test records in the app, and dry-run again. Iterating on 5 rows costs seconds; discovering a mapping bug after 2,000 rows costs an afternoon. ## Step 6: Run the full import Once the dry run looks right: ```bash node import.js my-export.csv clients node import.js my-leads.csv leads ``` The script prints progress per row and a summary at the end. If any rows fail, fix the data in `failed-rows.json` (or in your CSV), then re-run — the dedupe check means already-imported rows are skipped, so re-running is harmless. ## Step 7: Verify in the app When the script finishes, confirm the migration in the app, not just in the terminal: 1. **Counts match.** The Clients and Leads sections should show roughly your old CRM's totals (minus skipped duplicates and held-aside rows). The script's `created` plus `skipped` numbers should account for every row. 2. **Spot-check 10 random records.** Open a few clients and leads you know well — right name, right email, right phone. 3. **Search works.** Search for a couple of names and emails; if search finds them, the fields landed where the app expects them. ## Import checklist **Before** - Export and clean your CSV (blanks, duplicates, bad emails) - Decide lead vs client for every row - Check field names against [the reference](/api/reference) - Create an API key with the scopes the import needs — for example `{"clients":["read","write"],"leads":["read","write"]}`; a key with no scopes gets `403` on everything — and set `ACTUALLYCARE_API_KEY` **During** - Dry-run 5 rows and verify in the app before the full run - Watch the log for repeated failures — stop and fix rather than letting hundreds of rows fail the same way - Keep the pacing — don't "speed it up" past the rate limit **After** - Verify counts, spot-check records, test search - Re-run failed rows from `failed-rows.json` - Keep your original CSV export until you're confident — it's your backup --- # Set up webhooks > Register a webhook, verify signatures, and process events reliably. Webhooks push events to your application the moment they happen — a lead comes in, an escrow status changes, an appointment is booked — instead of you polling the API. This guide takes you from zero to a verified, retry-safe webhook receiver. For the conceptual background (what the catalog covers, payload envelope, delivery semantics), see [Webhooks](/concepts/webhooks). > **Who can do this:** webhook management (creating webhooks, listing the event catalog, reading delivery logs) requires a **broker** or **system-admin** role. Agents on a team will get `403` on every `/v1/webhooks` endpoint — ask your broker to set the webhook up, or to run these steps with you. > > **Auth:** the `/v1/webhooks` endpoints accept a **JWT bearer token only** (from [`POST /v1/auth/login`](/api/authentication)) — an `X-API-Key` header gets `401` here. The examples below assume the token is in `$ACTUALLYCARE_JWT`. ## Step 1: Discover the events you can subscribe to The event catalog is self-documenting. Ask the API what's available: ```bash curl "https://api.actuallycare.com/v1/webhooks/events" \ -H "Authorization: Bearer $ACTUALLYCARE_JWT" ``` The response uses the standard envelope, with `data` containing the full catalog — about 150 event types across roughly 27 categories, following an `entity.action` naming pattern (`escrow.*`, `lead.*`, `client.*`, `listing.*`, `appointment.*`, `care_status.*`, and more). There's also a grouped view: ```bash curl "https://api.actuallycare.com/v1/webhooks/events/categories" \ -H "Authorization: Bearer $ACTUALLYCARE_JWT" ``` Pick the specific events you care about, or subscribe to `all` if you want everything. ## Step 2: Register your webhook You can register in the app or via the API. Either way you'll provide three things: the URL to deliver to, the events to subscribe to, and a secret (minimum 10 characters) used to sign every delivery. ### In the app 1. Open **Settings → Webhooks** (direct link: `https://app.actuallycare.com/settings?tab=webhooks`). 2. Add a webhook with your endpoint URL, the events you chose in Step 1, and a secret. The Webhooks tab is available to broker and system-admin roles, and webhooks are team-scoped — one registration covers events for your whole team. ### Via the API ```bash curl -X POST "https://api.actuallycare.com/v1/webhooks" \ -H "Authorization: Bearer $ACTUALLYCARE_JWT" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/actuallycare", "events": ["lead.created", "escrow.updated"], "secret": "a-long-random-secret-string" }' ``` Use exact event names from the Step 1 catalog. The response envelope's `data` contains your new webhook, including its `id` — keep that for managing it later (`PUT /v1/webhooks/:id`, `DELETE /v1/webhooks/:id`, and the logs endpoint in Step 7). Generate the secret randomly (for example `openssl rand -hex 32`) and store it in an environment variable on your server. You'll need it to verify signatures next. ## Step 3: Receive and verify deliveries Every delivery is an HTTP POST with this payload envelope: ```json { "event": "lead.created", "team_id": "...", "timestamp": "2026-06-11T17:42:11.000Z", "data": {} } ``` And these headers: | Header | Contents | |---|---| | `X-Webhook-Signature` | `sha256=` followed by the HMAC-SHA256 hex digest of the JSON body, keyed with your secret (e.g. `sha256=3f5a…`). | | `X-Webhook-Event` | The event name, so you can route before parsing the body. | | `X-Webhook-Timestamp` | When the delivery was sent. | **Always verify the signature** with a timing-safe comparison, computed over the raw request body exactly as received — re-serializing the parsed JSON can change byte order or whitespace and break the match. ### Express ```javascript const express = require('express'); const crypto = require('crypto'); const app = express(); const SECRET = process.env.ACTUALLYCARE_WEBHOOK_SECRET; // Keep the raw bytes for signature verification. app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; }, })); function verifySignature(req) { // Header format: "sha256=" const received = (req.get('X-Webhook-Signature') || '').replace(/^sha256=/, ''); const expected = crypto .createHmac('sha256', SECRET) .update(req.rawBody) .digest('hex'); const a = Buffer.from(received, 'hex'); const b = Buffer.from(expected, 'hex'); return a.length === b.length && crypto.timingSafeEqual(a, b); } app.post('/webhooks/actuallycare', (req, res) => { if (!verifySignature(req)) { return res.status(401).send('invalid signature'); } res.status(200).send('ok'); // acknowledge first setImmediate(() => processEvent(req.body)); // work after responding }); function processEvent(payload) { console.log(`${payload.event} for team ${payload.team_id} at ${payload.timestamp}`); } app.listen(3000); ``` ### Flask ```python import hashlib import hmac import os from flask import Flask, request app = Flask(__name__) SECRET = os.environ["ACTUALLYCARE_WEBHOOK_SECRET"].encode() def verify_signature(req): # Header format: "sha256=" received = req.headers.get("X-Webhook-Signature", "").removeprefix("sha256=") expected = hmac.new(SECRET, req.get_data(), hashlib.sha256).hexdigest() return hmac.compare_digest(received, expected) @app.route("/webhooks/actuallycare", methods=["POST"]) def webhook(): if not verify_signature(request): return "invalid signature", 401 payload = request.get_json() # Hand off to a queue or worker — don't do slow work in the request. enqueue_for_processing(payload) return "ok", 200 ``` ## Step 4: Respond fast Deliveries time out after **30 seconds**, and a timeout counts as a failed delivery. Don't make ActuallyCare wait while you update your database, call another API, or send an email. The pattern: validate the signature, return `200`, then do the real work asynchronously — `setImmediate` in Node, a task queue or background worker in Python. Both examples above follow it. ## Step 5: Handle retries idempotently Delivery is **at-least-once**. A failed delivery — a network error, or a `5xx`, `408`, or `429` response from your endpoint — is retried at 1, 5, and 15 minutes after the initial attempt (up to 4 attempts total), then given up. Other `4xx` responses are treated as a rejection and are **not** retried, so don't return `400` for events you simply haven't handled yet. Retries survive server restarts; deactivating the webhook cancels any pending retries. The delivery logs (Step 7) show every attempt. At-least-once means your endpoint **will** occasionally see the same event twice — for example if your handler responded slowly and the delivery timed out after you'd already started processing. A retried delivery carries the same `X-Webhook-Timestamp` header and signature as the original, so deduplicate on the **signature header value** — unlike `event` + `timestamp`, it can't collide when two different records fire the same event in the same instant: ```javascript const seen = new Set(); // use a database or cache with a TTL in production function handleDelivery(req) { const key = req.get('X-Webhook-Signature'); // stable across retries, can't collide if (seen.has(key)) return; // already handled — a retry, skip it seen.add(key); processEvent(req.body); // ...actual processing } ``` An in-memory set works for a single process; use your database or a shared cache once you run more than one instance. ## Step 6: Test locally Webhook URLs must be reachable from the internet, but you can develop against your laptop with any tunneling tool (ngrok, Cloudflare Tunnel, or similar): 1. Start your receiver locally (port 3000 in the Express example). 2. Start a tunnel to that port — the tool gives you a public HTTPS URL. 3. Register that URL as a webhook (Step 2). You can register a second, temporary webhook just for development and delete it when you're done. 4. Trigger a real event — create a test lead in the app — and watch the delivery arrive in your terminal. When you change tunnels (most tools issue a new URL per session), update the webhook with `PUT /v1/webhooks/:id` or in Settings → Webhooks. ## Step 7: Monitor deliveries Two places to look when you think a delivery went missing: - **In the app:** Settings → Webhooks shows delivery logs per webhook — what was sent, when, and whether your endpoint accepted it. - **Via the API:** ```bash curl "https://api.actuallycare.com/v1/webhooks/WEBHOOK_ID/logs" \ -H "Authorization: Bearer $ACTUALLYCARE_JWT" ``` If deliveries are failing, the usual suspects in order: your endpoint isn't returning 200 within 30 seconds, the URL isn't publicly reachable (firewall, expired tunnel), or your signature check is rejecting valid payloads because it verified a re-serialized body instead of the raw bytes. For a full integration that combines webhooks with the REST API, continue to [Build a custom integration](/guides/build-custom-integration). --- # MCP overview > What the ActuallyCare MCP server exposes, how Claude connects to it, and where to start. MCP (Model Context Protocol) is an open standard that lets AI assistants like Claude work safely with outside systems. ActuallyCare runs an MCP server, which means Claude can look up your escrows, check today's appointments, or draft a follow-up for a hot lead — using your real CRM data, with your permission, under your account. If you just want to connect Claude, you only need one thing — the connector URL: ```text https://mcp.actuallycare.com/mcp ``` Paste it into Claude following the [quickstart](/mcp/quickstart) and you'll be connected in a few minutes. The rest of this page is the map of what's behind that URL. ## What the server exposes **Tools.** Actions covering every part of the CRM, named by entity and verb: `escrows_list`, `clients_create`, `leads_convert`, `appointments_today`, and so on. The [tool reference](/tools) is generated straight from the server's registry, so the live counts and parameters there are always current. **Prompts.** 13 built-in prompts — ready-made workflows like `create_escrow`, `daily_summary`, `hot_leads_followup`, and `closing_checklist` that package a multi-step task into one request. Each has a generated reference page under [Prompts](/prompts). ## How Claude finds tools To keep conversations fast and cheap, the server doesn't push the full catalog into every chat. Instead it advertises a short hot-list of the most-used tools plus a few discovery tools, and everything else stays fully callable on demand. In practice: Claude sees the most-used tools up front and finds the rest when it needs them. You don't have to manage any of this — ask for what you want and Claude locates the right tool. ## Connection details | | | | --- | --- | | Server URL | `https://mcp.actuallycare.com/mcp` | | Transport | Streamable HTTP (JSON-RPC over POST, with an SSE stream on the same endpoint) | | Legacy transport | HTTP+SSE at `https://mcp.actuallycare.com/sse`, for older clients | | Protocol version | 2025-03-26 | ## Authentication - **OAuth 2.1** — the normal path for Claude users. Adding the connector opens an ActuallyCare login and a consent screen listing exactly what Claude may do. Access tokens last 24 hours and refresh tokens rotate automatically, so you stay connected without re-approving. To disconnect, remove the connector in Claude's own settings. - **API key** — custom clients can send the same 64-character keys the REST API uses, in the `X-API-Key` header. See [Authentication](/api/authentication). - **JWT** — a Bearer token from `POST /v1/auth/login` also works, which is handy when your app already manages ActuallyCare user sessions. ## Supported clients | Client | Status | Guide | | --- | --- | --- | | Claude.ai (web) | Supported | [Connect Claude on the web](/mcp/claude-web) | | Claude Desktop | Supported | [Connect Claude Desktop](/mcp/claude-desktop) | | Claude mobile (iOS and Android) | Supported — connectors added on the web appear in the app automatically | [Use ActuallyCare on mobile](/mcp/claude-mobile) | | Claude Code | Supported | [Connect Claude Code](/mcp/claude-code) | | Any standards-compliant MCP client | Supported | [Build a custom MCP app](/mcp/custom-apps) | | ChatGPT | Not yet | — | | Gemini | Not yet | — | ## Where to go next - **Agents and non-technical users:** the [quickstart](/mcp/quickstart) walks you through connecting Claude step by step, and [What can I ask?](/agents/what-can-i-ask) shows what to do once you're connected. - **Developers:** [Build a custom MCP app](/mcp/custom-apps) covers connecting from your own code, and [API vs MCP](/concepts/api-vs-mcp) explains when to use the REST API instead. --- # Connect Claude Code > Add the ActuallyCare MCP server to Claude Code. Claude Code is Anthropic's terminal-based coding agent. Pointing it at the ActuallyCare MCP server gives you a CRM you can query from the command line, script against, and wire into automations. This page is for developers — if you want the no-terminal setup, start at the [quickstart](/mcp/quickstart) instead. ## Add the server One command, run anywhere: ```bash claude mcp add --transport http actuallycare https://mcp.actuallycare.com/mcp ``` This registers the server under the name `actuallycare` using the Streamable HTTP transport. By default the entry is scoped to the current project for you only; see [Project-scoped config](#project-scoped-config) for sharing it with a team. ## Authenticate The server uses OAuth, and Claude Code handles the browser dance for you. 1. Start Claude Code and run the `/mcp` command, then select **actuallycare**. **What you should see:** The MCP server list with `actuallycare` showing as needing authentication, and an option to authenticate. 2. Choose authenticate. Your browser opens an ActuallyCare page — sign in with your ActuallyCare credentials if asked, then approve the **Grant Claude access to ActuallyCare** consent screen. **What you should see:** A success page in the browser, and back in the terminal, `actuallycare` showing as connected. The OAuth callback runs on a temporary localhost port that Claude Code opens for you, and the server accepts localhost redirect URIs — no redirect-URI setup needed. Tokens refresh automatically, so this is a one-time step per machine. ## Verify ```bash claude mcp list ``` **What you should see:** output similar to: ```text actuallycare: https://mcp.actuallycare.com/mcp (HTTP) - ✓ Connected ``` Then prove it end to end inside a session: ```text > Using ActuallyCare, list my escrows closing in the next 14 days. ``` Claude calls `escrows_list` and returns live data. Note there is no `--mcp-list` flag — manage servers with the `claude mcp` commands shown above. ## Project-scoped config To check the server into a repo so every collaborator gets it, use project scope: ```bash claude mcp add --transport http --scope project actuallycare https://mcp.actuallycare.com/mcp ``` This writes `.mcp.json` at the project root: ```json { "mcpServers": { "actuallycare": { "type": "http", "url": "https://mcp.actuallycare.com/mcp" } } } ``` Commit it. Each developer still authenticates individually via `/mcp`, so everyone sees their own CRM data under their own permissions. ## What you can do with it - **Ad-hoc CRM queries while you work.** "Which listings expire this month?", "Pull the contact info for the buyer on the Maple St escrow." - **Scripted, non-interactive queries** with print mode: ```bash claude -p "Using ActuallyCare, list my hot leads as a markdown table with name, phone, and source" ``` Pipe that into files, cron jobs, or Slack notifiers. - **Building automations and integrations.** Claude Code can call ActuallyCare tools while it writes code against them — e.g. "fetch one real client record with `clients_get`, then generate a TypeScript interface for it." Tool names follow an `entity_verb` convention — `escrows_list`, `clients_create`, `leads_convert`, `appointments_today`, `get_dashboard_stats`. The server advertises the most-used tools up front and lets Claude discover the rest on demand, so don't worry if the initial tool list looks short. Browse the full reference at [/tools](/tools). ## Headless use with an API key OAuth is right for interactive use, but CI jobs and servers can't click through a browser. The MCP server also accepts an ActuallyCare API key in an `X-API-Key` header: ```bash claude mcp add --transport http actuallycare https://mcp.actuallycare.com/mcp \ --header "X-API-Key: $ACTUALLYCARE_API_KEY" ``` Or in `.mcp.json`, with environment-variable expansion so the key never lands in the repo: ```json { "mcpServers": { "actuallycare": { "type": "http", "url": "https://mcp.actuallycare.com/mcp", "headers": { "X-API-Key": "${ACTUALLYCARE_API_KEY}" } } } } ``` Create a key at [app.actuallycare.com](https://app.actuallycare.com) → Settings → API Keys (`/settings?tab=api`). Keys are 64-character hex strings shown once at creation — store the full value immediately, and make sure the key is granted the scopes it needs (a key with no scopes gets `403` everywhere). Every user can create their own keys (Settings → API Keys → Create API Key). Full details in [API authentication](/api/authentication). ## Troubleshooting | Symptom | Fix | |---|---| | `claude mcp list` shows the server as failed or needing auth | Run `/mcp` inside a session and re-authenticate. | | Connection errors right after adding | Confirm you used `--transport http` and the exact URL `https://mcp.actuallycare.com/mcp`. | | Worked, then 401s in a headless setup | The API key was likely revoked or expired — create a new one in Settings → API Keys. | | Claude answers without calling tools | Say "Using ActuallyCare, ..." explicitly, or check the server is connected with `/mcp`. | ## Next steps - [Custom apps](/mcp/custom-apps) — talk to the MCP server from your own code instead of Claude Code - [API vs MCP](/concepts/api-vs-mcp) — when to use the REST API instead - [Tool reference](/tools) — every tool, generated from the live registry --- # Connect Claude Desktop > Set up the connector in the Claude Desktop app. Claude Desktop is the Claude app you install on your Mac or PC. Connecting it to ActuallyCare uses the exact same flow as the web: **Settings → Connectors → Add custom connector**. There are no files to edit and nothing to install beyond the app itself. ## Check first — it may already be connected Connectors belong to your Claude *account*, not to one device. If you already added ActuallyCare on [claude.ai](/mcp/claude-web) and you're signed in to Claude Desktop with the same account, the connector is often already there. 1. Open Claude Desktop and start a new chat, then open the tools menu (the small sliders icon near the message box). **What you should see:** If **ActuallyCare** appears in the list, you're already set — skip down to [Verify it works](#verify-it-works). If it's not there, continue below. ## Add the connector in Claude Desktop 1. Open Claude Desktop and make sure you're signed in. **What you should see:** Claude's chat screen, the same layout as the website. 2. Open **Settings** and go to the **Connectors** section. **What you should see:** A list of your connectors with an **Add custom connector** button. 3. Click **Add custom connector**, name it "ActuallyCare", and paste `https://mcp.actuallycare.com/mcp` into the URL field. Then confirm. **What you should see:** Your browser opens an ActuallyCare sign-in window. This is your CRM asking for permission — you sign in with your ActuallyCare email and password, not your Claude login. 4. Sign in to ActuallyCare if asked, then approve the screen titled **Grant Claude access to ActuallyCare**. It lists four permissions: read your CRM data (escrows, listings, clients, leads, and calendar); create and update records when you ask; draft emails and texts for your approval; and archive or delete records, with confirmation. **What you should see:** The browser window closes or tells you you're done, and ActuallyCare shows as connected in Claude Desktop's Connectors list. ## Verify it works 1. Start a **new chat** in Claude Desktop, open the tools menu, and make sure **ActuallyCare** is toggled on. **What you should see:** ActuallyCare in the tools list with its toggle on. 2. Ask: "Using ActuallyCare, give me a quick summary of my dashboard." **What you should see:** Claude may ask permission to use an ActuallyCare tool — choose Allow. Then it answers with your real numbers: leads, escrows, appointments. From here, anything in [What can I ask?](/agents/what-can-i-ask) works in Desktop exactly as it does on the web. The connection stays signed in on its own — you won't be asked to approve again during normal use. ## Managing or removing the connection Turning ActuallyCare off for a single conversation, or disconnecting it entirely, works the same as on the web — it's all done from Claude's own settings, and removing the connector there cuts off access from every device at once. See [managing the connection](/mcp/claude-web#manage-the-connection) for the steps. ## If something didn't work See the [troubleshooting page](/agents/troubleshooting), especially: - [I can't find the Connectors option in Claude](/agents/troubleshooting#i-cant-find-the-connectors-option-in-claude) — including when the connector shows on the web but not in Desktop (usually a different-account issue) - [It connected but answers seem generic](/agents/troubleshooting#it-connected-but-answers-seem-generic) - [It says I'm unauthorized or my session expired](/agents/troubleshooting#it-says-im-unauthorized-or-my-session-expired) --- # Connect Claude on your phone > Use your CRM connector from the Claude mobile app. Your phone is where this connection earns its keep — between showings, in the car, walking out of a listing appointment. The Claude mobile app (iOS and Android) can use your ActuallyCare connector, with one catch: **you can't add the connector from inside the mobile app**. You add it once on claude.ai in a browser, and it then appears in the app automatically. ## First, add the connector on the web 1. Add the ActuallyCare connector on claude.ai by following [Connect Claude.ai (web)](/mcp/claude-web). You can do this in the browser on your phone — it doesn't need a computer. **What you should see:** ActuallyCare listed as connected under Settings → Connectors on claude.ai. If you've already done the web setup, there's nothing to repeat. Connectors belong to your Claude account, so the one you added on the web travels with you. ## Find it in the Claude mobile app 1. Open the Claude app on your phone and make sure you're signed in with the **same Claude account** you used on claude.ai. **What you should see:** Claude's chat screen. Same-account matters — a connector added under one account won't show up under another. 2. Start a new chat and tap the tools menu — the small sliders icon near the message box. **What you should see:** A list of tools and connectors, with **ActuallyCare** among them. You can toggle it on or off per conversation right here. 3. Make sure ActuallyCare is toggled on, then ask: "Using ActuallyCare, what's my next appointment?" **What you should see:** Claude may ask permission to use an ActuallyCare tool — tap Allow. Then it answers with your real schedule. ## Made for the road The mobile app supports voice dictation, which turns Claude into a hands-free CRM assistant. Tap the microphone and talk. Some real-world asks: **Right after a showing**, before you forget what the buyers said: > "Using ActuallyCare, log showing feedback for 42 Oak Ave — the buyers loved the kitchen, thought the backyard felt small, and want a second look this weekend." **Driving between appointments** (passenger seat or parked, please): > "What's my next appointment, and what's the address?" **Leaving an open house** with a pocket full of new names: > "Create a lead: Maria Lopez, phone 555-0142, met her at the Oak Avenue open house, looking for three bedrooms under seven fifty." **Walking up to a client's door**, needing a refresher: > "Give me a quick summary of everything we have on the Nguyens." **Waiting in the title office:** > "What's the status of the Hendersons' escrow? Anything due this week?" A note on safety: everything Claude logs or creates lands in your CRM where you can see and edit it. The connection works as your account — the same access you approved at setup — and anything outbound, like an email, starts as a draft until you give a clear okay. Details in [Permissions and safety](/agents/permissions-and-safety). ## If it isn't showing up The usual cause is two different Claude accounts — one on the web, another on the phone. Check the account email in the app's settings against the one on claude.ai. For everything else, see [I can't find the Connectors option in Claude](/agents/troubleshooting#i-cant-find-the-connectors-option-in-claude) and the rest of the [troubleshooting page](/agents/troubleshooting). --- # Connect Claude.ai (web) > Set up the connector on claude.ai in your browser. This is the full walkthrough for connecting ActuallyCare to Claude on the web at [claude.ai](https://claude.ai). It covers the same setup as the [5-minute quickstart](/mcp/quickstart), at a slower pace, plus how to manage the connection afterward — turning it on and off per conversation, and disconnecting it completely. The web setup is the one to learn first. The connector you add here belongs to your Claude account, so it also shows up in the [Claude desktop app](/mcp/claude-desktop) and the [Claude mobile app](/mcp/claude-mobile) when you're signed in with the same account. ## What you need - Your ActuallyCare login — the email and password you use at [app.actuallycare.com](https://app.actuallycare.com) - A Claude account at [claude.ai](https://claude.ai) You won't need anything technical. There are no codes to copy except one web address, and nothing you do here can delete or change your CRM data. ## Add the connector 1. Go to [app.actuallycare.com](https://app.actuallycare.com) in your browser and sign in. **What you should see:** Your ActuallyCare dashboard. You're doing this first so the sign-in step later happens automatically. 2. In a new tab, go to [claude.ai](https://claude.ai) and sign in to Claude. **What you should see:** Claude's chat screen, with a box at the bottom or middle where you'd normally type a message. 3. Open Claude's **Settings**. (It's usually behind your name or initials in the corner of the screen.) **What you should see:** A settings page with sections listed down the side. 4. Click the **Connectors** section. **What you should see:** A page listing connectors. If you've never added one, the list is empty — that's normal. There's a button labeled **Add custom connector**. 5. Click **Add custom connector**. **What you should see:** A small form asking for a name and a URL. 6. Type a name you'll recognize — "ActuallyCare" works — and paste `https://mcp.actuallycare.com/mcp` into the URL field, exactly as written. Then confirm. **What you should see:** A new window or tab opens. This is the ActuallyCare sign-in and permission window — the next section explains what's in it. ## What the sign-in window looks like This part trips people up, so here's exactly what to expect. The window that opens is an **ActuallyCare** page, not a Claude or Anthropic page. You're signing in to *your CRM* to tell it that Claude is allowed in. So: - If you weren't already signed in to ActuallyCare, you'll see the normal ActuallyCare login screen first. Use your ActuallyCare email and password — not your Claude login. - Once you're signed in, you'll see a permission screen titled **Grant Claude access to ActuallyCare**. 7. Read the permission screen. It lists exactly four permissions: - Read your CRM data — escrows, listings, clients, leads, and calendar - Create and update records when you ask - Draft emails and texts for your approval — nothing sends without your okay - Archive or delete records, with confirmation Approve it when you're ready. **What you should see:** The window closes on its own, and back in Claude's settings, ActuallyCare now appears in your Connectors list as connected. Once you approve, you stay connected. Behind the scenes the connection refreshes itself automatically, so you won't be asked to approve again during normal use. For more on what Claude can and can't do once connected, see [Permissions and safety](/agents/permissions-and-safety). ## Use it in a conversation 8. Start a **new chat** in Claude. **What you should see:** A fresh, empty conversation. 9. Open the tools menu — the small sliders icon near the message box — and check that **ActuallyCare** is switched on. **What you should see:** ActuallyCare in the list of tools with its toggle on. If it's off, tap the toggle. 10. Ask something only your CRM would know, like: "Using ActuallyCare, what's on my calendar today?" **What you should see:** Claude may ask permission to use an ActuallyCare tool the first time — choose Allow. Then it answers with your real appointments. Two useful things to know about how this works: - **Saying "Using ActuallyCare" helps.** Once Claude has used the connector a few times in a chat, you can drop it and just talk naturally. - **Claude finds tools as it needs them.** ActuallyCare gives Claude its most-used tools up front and lets it find the rest on demand, so you never have to pick tools yourself. Not sure what to ask? Start with [What can I ask?](/agents/what-can-i-ask) ## Manage the connection ### Turn it off for one conversation You don't have to disconnect to keep ActuallyCare out of a particular chat. In any conversation, open the tools menu and switch the ActuallyCare toggle off. That conversation won't touch your CRM; your other conversations are unaffected. ### Disconnect completely 1. In claude.ai, open **Settings**, then **Connectors**. **What you should see:** Your connectors list, with ActuallyCare in it. 2. Find ActuallyCare and choose the option to remove or disconnect it. **What you should see:** ActuallyCare disappears from the list. Claude can no longer reach your CRM from any device. Disconnecting happens entirely on Claude's side — there's no separate switch to flip inside ActuallyCare. If you change your mind later, just add the connector again; it takes the same five minutes. ## If something didn't work Head to the [troubleshooting page](/agents/troubleshooting) for fixes to the most common problems: - [I can't find the Connectors option in Claude](/agents/troubleshooting#i-cant-find-the-connectors-option-in-claude) - [It connected but answers seem generic](/agents/troubleshooting#it-connected-but-answers-seem-generic) - [It says I'm unauthorized or my session expired](/agents/troubleshooting#it-says-im-unauthorized-or-my-session-expired) - [How do I disconnect or reconnect?](/agents/troubleshooting#how-do-i-disconnect-or-reconnect) --- # Build a custom MCP app > Connect your own application to the ActuallyCare MCP server with the TypeScript SDK, choose an auth method, and drive the tools with the Anthropic API. The ActuallyCare MCP server isn't just for Claude. Any standards-compliant MCP client can connect to it, which means you can build things like: - **An internal dashboard** that asks the CRM questions in plain English and renders the answers - **A voice assistant** that looks up escrows and schedules showings while an agent is driving between appointments - **A nightly AI analyst** that pulls pipeline stats, flags stalled deals, and emails a summary before the morning standup The shape is always the same: your app speaks MCP to `https://mcp.actuallycare.com/mcp` over Streamable HTTP — JSON-RPC requests via POST, with an SSE stream for server-initiated messages. A legacy HTTP+SSE transport also exists at `/sse` for older clients. ## Quickstart: a TypeScript MCP client Install the official MCP TypeScript SDK ([github.com/modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk)): ```bash npm install @modelcontextprotocol/sdk ``` Then connect, list the tools, and call one. This example authenticates with an API key in the `X-API-Key` header (see [Authentication](#authentication) below for the other options): ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const mcp = new Client({ name: "my-actuallycare-app", version: "1.0.0" }); const transport = new StreamableHTTPClientTransport( new URL("https://mcp.actuallycare.com/mcp"), { requestInit: { headers: { "X-API-Key": process.env.ACTUALLYCARE_API_KEY! }, }, }, ); await mcp.connect(transport); // Discover the advertised tools const { tools } = await mcp.listTools(); console.log("Advertised tools:", tools.map((t) => t.name).join(", ")); // Call one const result = await mcp.callTool({ name: "escrows_list", arguments: {}, }); console.log(JSON.stringify(result.content, null, 2)); await mcp.close(); ``` You should see the advertised tool names first — entity_verb names like `escrows_list`, `clients_list`, `appointments_today`, plus discovery tools like `discover_tools` — followed by the tool result. Tool results come back as MCP content blocks: an array of text items whose `text` carries the data as JSON. ```json [ { "type": "text", "text": "..." } ] ``` One important nuance: `tools/list` does **not** return everything. The server advertises a short hot-list of the most-used tools plus discovery meta-tools, and every other tool stays fully callable by name. Browse the full surface at [/tools](/tools) or fetch it as machine-readable JSON from [/tools.json](/tools.json). ## Authentication The MCP server accepts three credentials. Pick based on who the app acts as: | Method | Best for | Header | |---|---|---| | API key | Server-side apps acting as one account (dashboards, cron jobs) | `X-API-Key` | | JWT bearer token | Apps where users log in with their ActuallyCare credentials | `Authorization: Bearer ...` | | OAuth 2.1 | Apps users authorize without sharing credentials | `Authorization: Bearer ...` | ### API key (simplest) API keys are 64-character hex strings. Create one in the app under Settings → API Keys (`https://app.actuallycare.com/settings?tab=api`). Every user can create their own keys: Settings → API Keys → Create API Key. The full key is shown once at creation, so store it immediately; the UI only displays the first 8 and last 4 characters afterward. Pass the key in the `X-API-Key` header on every request, as in the quickstart above. See [API authentication](/api/authentication) for key creation via the REST API and key management details. ### JWT bearer token (user-session apps) If your app already signs users in against the ActuallyCare REST API, the same JWT works against the MCP server. Log in via the REST API to get an access token, then send it as `Authorization: Bearer` instead of `X-API-Key`: ```typescript const transport = new StreamableHTTPClientTransport( new URL("https://mcp.actuallycare.com/mcp"), { requestInit: { headers: { Authorization: `Bearer ${accessToken}` }, }, }, ); ``` Access-token lifetime is role-based (15 minutes to 8 hours), so build in token refresh. The login and refresh flows are documented in [API authentication](/api/authentication). ### OAuth 2.1 (user-delegated apps) For apps acting on behalf of users who shouldn't hand you their password, the MCP server supports OAuth 2.1 with PKCE and dynamic client registration. Register your client with `POST /register` on `mcp.actuallycare.com`, then run the standard authorization-code flow with PKCE: the user logs into ActuallyCare, approves a consent screen, and your app receives tokens scoped to that user. Request the scopes you need from `read`, `write`, and `mcp` (`mcp` is required for tool calls; the full list is published in the [authorization-server metadata](https://mcp.actuallycare.com/.well-known/oauth-authorization-server)). Access tokens last 24 hours and refresh tokens rotate automatically, so the connection stays live without re-approval. This is the same flow Claude itself uses when someone adds ActuallyCare as a connector. ## Drive the tools with Claude The most common custom app is a thin loop: send a question to Claude through the Anthropic API, hand it the ActuallyCare tool definitions, execute whatever tools it asks for against the MCP server, and feed the results back. ```bash npm install @anthropic-ai/sdk ``` ```typescript import Anthropic from "@anthropic-ai/sdk"; const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment // 1. Fetch tool definitions from the MCP server and convert to Anthropic's shape const { tools } = await mcp.listTools(); const anthropicTools = tools.map((t) => ({ name: t.name, description: t.description ?? "", input_schema: t.inputSchema, })); // 2. The tool-use loop let messages: Anthropic.MessageParam[] = [ { role: "user", content: "Which of my escrows are closing soonest?" }, ]; while (true) { const response = await anthropic.messages.create({ model: "claude-sonnet-4-6", max_tokens: 16000, tools: anthropicTools, messages, }); if (response.stop_reason !== "tool_use") { for (const block of response.content) { if (block.type === "text") console.log(block.text); } break; } // 3. Execute each tool_use block against the MCP server messages.push({ role: "assistant", content: response.content }); const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { const result = await mcp.callTool({ name: block.name, arguments: block.input as Record, }); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result.content), }); } } // 4. Return the results and let Claude continue messages.push({ role: "user", content: toolResults }); } ``` Because `listTools()` only returns the advertised hot-list, Claude starts with the most-used tools. That's usually what you want — but if your app needs the whole surface up front, build your tool array from [/tools.json](/tools.json) instead. Every documented tool is callable by name whether or not it's advertised. For the full tool-use pattern — streaming, parallel tool calls, error handling — see the Anthropic documentation at [docs.anthropic.com](https://docs.anthropic.com). ## Operational notes | Limit | Value | |---|---| | Requests | 120 per minute per user | | Tool calls | 30 per minute per user | | Tool execution timeout | 300 seconds | A few things to design around: - **Back off on rate-limit errors.** The per-user limits above apply across everything that user's credentials touch, so a busy dashboard and a background job sharing one key share one budget. - **Long-running tools.** Most tools return in well under a second, but the server allows up to 300 seconds per call — keep your own HTTP timeouts above that if you call heavyweight tools. - **Progressive disclosure.** As covered above, `tools/list` advertises a hot-list and discovery tools find the rest on demand. Consult [/tools.json](/tools.json) for the complete, always-current catalog. - **Built-in prompts.** The server ships ready-made prompt templates — `daily_summary`, `escrow_status_check`, `hot_leads_followup`, and more — that your app can list and invoke via the standard MCP prompts capability. The full reference is at [/prompts](/prompts). ## Security - **Keep credentials server-side.** Never put an API key, JWT, or OAuth token in browser JavaScript — anything shipped to the client is readable by anyone who opens dev tools. Browser apps should talk to your backend, and your backend talks to the MCP server. - **Scope keys minimally.** Scopes are what **grant** a key access — a key created without scopes has none and gets `403` on everything. Grant each app the narrowest scopes that work (for example `{"all":["read"]}` for a read-only dashboard, or `{"clients":["read","write"]}` for a clients-only app), and set an expiration rather than minting forever-keys — a key without one never expires. - **One key per app.** Separate keys per integration make rotation painless and let you revoke a compromised app without breaking the others. - **Treat keys like passwords.** They're stored hashed and shown once — if a key leaks, revoke it and create a new one. --- # Connect Claude in 5 minutes > Link Claude to your ActuallyCare CRM, step by step. This page connects Claude — the AI assistant from Anthropic — to your ActuallyCare account. Once connected, you can ask Claude things like "what's on my calendar today?" and it answers straight from your CRM. You'll do everything in your web browser on claude.ai. A computer, tablet, or phone all work — on a phone, use the browser rather than the Claude app for this one-time setup. No downloads, no technical setup. It takes about five minutes, and you can undo it any time with one click in Claude's settings. ## Before you start You need three things: - Your ActuallyCare login (the email and password you use at [app.actuallycare.com](https://app.actuallycare.com)) - A Claude account at [claude.ai](https://claude.ai) - A paid Claude plan (Claude Pro or higher) — custom connectors aren't included in Claude's free plan. If you see Settings → Connectors in Claude, you're set. ## Connect Claude 1. Go to [app.actuallycare.com](https://app.actuallycare.com) and sign in to ActuallyCare. **What you should see:** Your ActuallyCare dashboard. Signing in now means you won't have to type your password again mid-setup. 2. Open a new browser tab and sign in at [claude.ai](https://claude.ai). **What you should see:** Claude's chat screen with a message box in the middle. 3. Open Claude's **Settings** — click your name or initials in the corner of the screen — then find the **Connectors** section. **What you should see:** A page listing any connectors you already have, with an **Add custom connector** button. 4. Click **Add custom connector**. The form asks for a name and a URL (web address): type **ActuallyCare** as the name, paste `https://mcp.actuallycare.com/mcp` into the URL field, and confirm. **What you should see:** A new window opens asking you to connect to ActuallyCare. If you weren't already signed in to ActuallyCare, it asks you to sign in first — use your normal ActuallyCare login, not your Claude login. 5. On the screen titled **Grant Claude access to ActuallyCare**, read the permissions and approve. It asks for exactly four things: - Read your CRM data — escrows, listings, clients, leads, and calendar - Create and update records when you ask - Draft emails and texts for your approval — nothing sends without your okay - Archive or delete records, with confirmation **What you should see:** The window closes and ActuallyCare appears in your Connectors list as connected. 6. Go back to Claude and start a new chat. Open the tools menu (the small sliders icon near the message box) and make sure **ActuallyCare** is turned on. **What you should see:** ActuallyCare listed in the tools menu with its toggle switched on. 7. Type this and send it: "Using ActuallyCare, what's on my calendar today?" **What you should see:** Claude may first ask for your permission to use an ActuallyCare tool — choose Allow. Then it lists your appointments for today, pulled live from your CRM. An empty-but-specific answer like "you have no appointments today" still counts as success. If you get general advice about how agents manage their calendars instead, it's not connected — see [It connected but answers seem generic](/agents/troubleshooting#it-connected-but-answers-seem-generic). That's it. You're connected. Claude sees the most-used tools up front and finds the rest when it needs them, so there's nothing else to configure. ## Try these first Five good first asks, in plain English: - "Show me my hot leads." - "Give me a summary of my dashboard — what needs my attention?" - "Which of my escrows are closing this month?" - "Pull up everything we have on the Hendersons." - "Create a lead: Maria Lopez, phone 555-0142, met her at the open house on Oak Avenue." For a much longer list of ideas, see [What can I ask?](/agents/what-can-i-ask) And if you're wondering what Claude is and isn't allowed to do with your data, read [Permissions and safety](/agents/permissions-and-safety). ## Using Claude somewhere other than the browser The connector you just added follows your Claude account around: - **On the Claude desktop app** — it's usually already there. See [Connect Claude Desktop](/mcp/claude-desktop). - **On your phone** — it shows up in the Claude mobile app automatically. See [Connect Claude on your phone](/mcp/claude-mobile). - **Developer in a terminal** — add it to Claude Code with one command. See [Connect Claude Code](/mcp/claude-code). ## If something didn't work The fixes for the most common problems are on the [troubleshooting page](/agents/troubleshooting): - The Connectors section or the connector itself is missing — see [I can't find the Connectors option in Claude](/agents/troubleshooting#i-cant-find-the-connectors-option-in-claude). - Nothing happened after you pasted the URL — it's usually a pop-up blocker. See [The ActuallyCare sign-in window never opened](/agents/troubleshooting#the-actuallycare-sign-in-window-never-opened). - Claude answers from general knowledge instead of your CRM — see [It connected but answers seem generic](/agents/troubleshooting#it-connected-but-answers-seem-generic). - It worked before and stopped — see [It says I'm unauthorized or my session expired](/agents/troubleshooting#it-says-im-unauthorized-or-my-session-expired). --- # Where to find the ActuallyCare MCP server > Every place the ActuallyCare MCP server is published or discoverable — the endpoint, the registry entry, machine-readable indexes, and support contacts. One MCP server, several ways to discover it. This page is the canonical list — if a tool, registry, or agent needs to find ActuallyCare, everything it needs is below. ## The server | | | | --- | --- | | Endpoint | `https://mcp.actuallycare.com/mcp` | | Transport | Streamable HTTP (a legacy HTTP+SSE endpoint exists at `/sse` for older clients) | | Auth | OAuth 2.1 with PKCE and dynamic client registration — clients discover everything from the `WWW-Authenticate` header and `/.well-known/oauth-authorization-server` | | Scopes | `read`, `write`, `mcp` | | Account required | An ActuallyCare login ([app.actuallycare.com](https://app.actuallycare.com)) | To connect from Claude, follow [Connect Claude in 5 minutes](/mcp/quickstart) (Claude.ai, Desktop, or mobile) or [Connect Claude Code](/mcp/claude-code). For any other MCP client, paste the endpoint URL and complete the OAuth flow in your browser. ## Official MCP Registry The server's canonical registry identity is **`com.actuallycare/actuallycare`** on the [official MCP Registry](https://registry.modelcontextprotocol.io) — the feed that downstream directories (Smithery, Glama, MCP.so) and client-side registry search mirror. The listing is live (published 2026-07-03, renamed from `com.actuallycare/crm` on 2026-07-04). ## Machine-readable surfaces For AI agents and crawlers, everything on this site is available in machine-friendly form: | Surface | URL | What it is | | --- | --- | --- | | Pointer file | [`/.well-known/ai.json`](https://docs.actuallycare.com/.well-known/ai.json) | One JSON file linking every surface in this table | | Tool index | [`/tools.json`](https://docs.actuallycare.com/tools.json) | Every MCP tool — names, descriptions, JSON schemas, safety annotations | | llms.txt | [`/llms.txt`](https://docs.actuallycare.com/llms.txt) | Index of all documentation pages with one-line summaries | | llms-full.txt | [`/llms-full.txt`](https://docs.actuallycare.com/llms-full.txt) | Every documentation page concatenated into one file | | OpenAPI spec | [`api.actuallycare.com/v1/openapi.json`](https://api.actuallycare.com/v1/openapi.json) | The REST API specification | | Markdown mirrors | append `.md` to any page URL | Raw Markdown of every page; also served via `Accept: text/markdown` | ## Support and policies - **Support:** [support@actuallycare.com](mailto:support@actuallycare.com) or [www.actuallycare.com/contact](https://www.actuallycare.com/contact) — questions about the MCP server, the API, or these docs. Signed-in users can also file issues from the in-app support widget. - **Privacy policy:** [www.actuallycare.com/privacy](https://www.actuallycare.com/privacy) - **Troubleshooting the connector:** [Troubleshooting](/agents/troubleshooting) --- # Prompt library > Thirteen built-in MCP prompts plus the best plain-English asks for your CRM. Once Claude is connected to ActuallyCare (five-minute setup: [quickstart](/mcp/quickstart)), there are two ways to get things done. Pick a ready-made prompt from Claude's menu, or skip the menu and ask in your own words. Both work. This page covers both. ## Built-in prompts The ActuallyCare MCP server ships 13 ready-made prompts. On claude.ai and in the Claude apps, they show up in Claude's prompt menu — in a chat, look behind the **+** or sliders icon near the message box. Each one is a guided starting point that already knows which tools to call and what to ask you for. **What you should see:** the ActuallyCare prompts listed by name — daily_summary, find_client, and the rest of the table below. The menu is optional. If you don't see it, just ask in your own words — same result. | Prompt | Reach for it when... | |---|---| | [daily_summary](/prompts/daily_summary) | Monday morning, coffee in hand — "what does today look like?" | | [hot_leads_followup](/prompts/hot_leads_followup) | You're worried leads are going cold and want a follow-up plan | | [find_client](/prompts/find_client) | You half-remember a name, or only have a phone number | | [client_portfolio](/prompts/client_portfolio) | You're about to call a client and want their full history first | | [lead_qualification](/prompts/lead_qualification) | A brand-new lead just came in and you want to qualify them properly | | [update_lead_status](/prompts/update_lead_status) | A lead just moved forward — or stalled — in your pipeline | | [create_escrow](/prompts/create_escrow) | You just opened escrow and want it in the system without missing a field | | [escrow_status_check](/prompts/escrow_status_check) | Someone asks "where are we on the Smith deal?" | | [closing_checklist](/prompts/closing_checklist) | Closing is a week out and you want nothing slipping through | | [document_request](/prompts/document_request) | An escrow is missing paperwork and you need to chase it down | | [schedule_showing](/prompts/schedule_showing) | A buyer wants to see a property | | [commission_report](/prompts/commission_report) | You want to know what's pending and what's been paid | | [market_snapshot](/prompts/market_snapshot) | You're prepping a listing presentation and need current numbers | ### What a daily_summary response looks like Here's a realistic example of the kind of answer **daily_summary** produces — your data will differ, but the shape is the same: > **Good morning! Here's your briefing for January 18, 2026:** > > **Today's Schedule (3 appointments):** > - 10:00 AM - Showing at 123 Oak St with Sarah Johnson > - 2:00 PM - Listing presentation at 456 Pine Ave > - 4:30 PM - Buyer consultation with Michael Chen > > **Tasks Due Today (2):** > - Follow up with lender on Johnson escrow > - Submit disclosure package for Pine Ave listing > > **Overdue Tasks (1):** > - Call Maria Rodriguez re: offer feedback (due 2 days ago) > > **Hot Leads to Contact (3):** > - David Kim - Last contacted 8 days ago (buyer, $800K budget) > - Jennifer Lee - Requested showing, no response yet > - Robert Taylor - Pre-approved, looking in Beverly Hills > > **Escrow Updates:** > - 789 Maple Dr: Appraisal scheduled for tomorrow > - 321 Elm St: Contingency removal due in 3 days > > **Quick Stats:** > - Active escrows: 4 > - Pipeline value: $3.2M > - Projected closings this month: 2 > > Would you like me to expand on any of these items? ## Or just ask You never need the menu. Claude understands plain English and finds the right tools on its own — the built-in prompts are shortcuts, not requirements. Some of the best asks, grouped by mood: ### Morning - "What's my day look like?" - "Anything urgent I should handle before my 10 a.m.?" ### Money - "What commissions are pending this quarter, and what's already been paid?" - "What's my active pipeline worth right now?" ### People - "Who haven't I talked to in 30 days?" - "Pull up everything we have on the Hendersons before I call them." - "Which of my leads are going cold?" ### Transactions - "Which escrows close this month, and is anything blocking them?" - "What deadlines are coming up on the Maple Drive escrow?" - "Draft a status update email for my seller on Pine Avenue." For a much longer list of ideas, see [What can I ask?](/agents/what-can-i-ask) And if you're wondering what Claude can and can't do with your data, read [Permissions and safety](/agents/permissions-and-safety). --- # client_portfolio > Get complete portfolio and history for a client Get complete portfolio and history for a client. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **client_portfolio** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Give me the full picture on the Garcias." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `client_name` | Yes | Client name or email | --- # closing_checklist > Generate pre-closing verification checklist for an escrow Generate pre-closing verification checklist for an escrow. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **closing_checklist** from the prompt menu — or skip the menu entirely and just ask in plain English: > "What's left before we can close 123 Main St?" ## Arguments | Argument | Required | Description | | --- | --- | --- | | `escrow_id` | Yes | Escrow ID or property address | --- # commission_report > Generate a commission report with pending and paid amounts Generate a commission report with pending and paid amounts. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **commission_report** from the prompt menu — or skip the menu entirely and just ask in plain English: > "How much commission do I have pending this month?" ## Arguments | Argument | Required | Description | | --- | --- | --- | | `period` | No | Time period (this_month, last_month, this_year, all) | --- # create_escrow > Guided workflow for creating a new escrow transaction Guided workflow for creating a new escrow transaction. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **create_escrow** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Open a new escrow for 123 Main St at $450,000." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `property_address` | Yes | Property address for the escrow | | `purchase_price` | Yes | Purchase price in dollars | | `buyer_name` | No | Name of the buyer | | `seller_name` | No | Name of the seller | --- # daily_summary > Generate a summary of today's activities, tasks, and appointments Generate a summary of today's activities, tasks, and appointments. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **daily_summary** from the prompt menu — or skip the menu entirely and just ask in plain English: > "What's my day look like?" ## Arguments _None — this prompt needs no input._ --- # document_request > Generate document request for an escrow Generate document request for an escrow. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **document_request** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Draft a request for the missing inspection report on the Smith escrow." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `escrow_id` | Yes | Escrow ID | | `doc_type` | No | Document type needed (optional) | --- # escrow_status_check > Check the status of an escrow and its pending tasks Check the status of an escrow and its pending tasks. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **escrow_status_check** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Where are we on the Johnson escrow?" ## Arguments | Argument | Required | Description | | --- | --- | --- | | `escrow_identifier` | Yes | Escrow number, property address, or client name | --- # find_client > Smart client lookup by name, email, or phone number Smart client lookup by name, email, or phone number. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **find_client** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Find my client Sarah — I think her email ends in @gmail.com." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `search_term` | Yes | Name, email, or phone to search for | --- # hot_leads_followup > Get a list of hot leads that need follow-up with suggested actions Get a list of hot leads that need follow-up with suggested actions. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **hot_leads_followup** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Who should I follow up with today?" ## Arguments _None — this prompt needs no input._ --- # lead_qualification > Guided lead qualification questionnaire Guided lead qualification questionnaire. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **lead_qualification** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Help me qualify the new lead from the open house." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `lead_name` | Yes | Lead name or email | --- # market_snapshot > Current real estate market statistics Current real estate market statistics. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **market_snapshot** from the prompt menu — or skip the menu entirely and just ask in plain English: > "How's the market in Bakersfield right now?" ## Arguments | Argument | Required | Description | | --- | --- | --- | | `area` | No | City or zip code (optional) | --- # schedule_showing > Schedule a property showing appointment Schedule a property showing appointment. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **schedule_showing** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Set up a showing at 42 Oak Ave for the Hendersons next Saturday afternoon." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `property_address` | Yes | Property address or MLS number | | `client_name` | Yes | Client name for the showing | | `preferred_date` | No | Preferred date (e.g., "tomorrow", "next Monday") | | `preferred_time` | No | Preferred time (e.g., "2pm", "afternoon") | --- # update_lead_status > Update the status of a lead in the pipeline Update the status of a lead in the pipeline. ## How to use it This is a built-in prompt on the ActuallyCare MCP server. In Claude clients that support MCP prompts you can pick **update_lead_status** from the prompt menu — or skip the menu entirely and just ask in plain English: > "Mark the lead Mike Torres as qualified — he got pre-approved today." ## Arguments | Argument | Required | Description | | --- | --- | --- | | `lead_identifier` | Yes | Lead name, email, or ID | | `new_status` | Yes | New status (new, contacted, qualified, converted, lost) | | `notes` | No | Notes about the status change | --- # MCP tool reference > Every tool the ActuallyCare MCP server exposes to Claude — 232 tools across 38 categories. When you connect Claude to ActuallyCare, it can use **232 tools** across **38 categories** — everything from looking up an escrow to drafting a review request. This reference documents every one of them. You don't need to memorize tool names. Just ask Claude in plain English — it picks the right tool. To keep conversations fast, Claude sees a short list of the most-used tools up front and finds the rest on demand, so don't worry if a tool here doesn't appear in Claude's tool list: it can still use it. A note on wording: each tool's description below is the **literal instruction text Claude reads** when deciding how to use the tool. Some of it is written *to Claude* ("Do NOT ask for missing fields…") rather than to you — we publish it verbatim so you can see exactly what Claude is told. Machine-readable index: [`tools.json`](/tools.json) --- # Agent brokerage history > Look up which brokerage an agent was at on a given date, from California DRE public records. Look up which brokerage an agent was at on a given date, from California DRE public records. ## Tools (2) | Tool | Type | What it does | | --- | --- | --- | | [`agent_brokerage_at_date`](/tools/agent-brokerage-history/agent_brokerage_at_date) | Read-only | Resolve which brokerage an agent was at on a specific date. | | [`agent_brokerage_history_list`](/tools/agent-brokerage-history/agent_brokerage_history_list) | Read-only | List the full brokerage career timeline for an agent. | --- # agent_brokerage_at_date > Resolve which brokerage an agent was at on a specific date. Resolve which brokerage an agent was at on a specific date. Returns the brokerage name, DRE license number, and date range of the matching responsible-broker relationship from CA DRE public records. Use this when a user asks "which brokerage was X at on Y date" or for historical escrow attribution. Returns null when the date predates the agent's recorded history (DRE typically goes back ~5–10 years). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `user_id` | string | No | Platform user UUID. Defaults to the calling user when omitted. | | `dre_license_id` | string | No | CA DRE 8-digit license number (alternative to user_id, useful for non-platform agents on a listing). | | `on_date` | string | Yes | Date to resolve, in YYYY-MM-DD format. Both period start_date and end_date are inclusive bounds. · Format: Date (YYYY-MM-DD) | ## Example prompts - "Which brokerage was I working under on March 15, 2022?" - "Which brokerage was the agent with DRE license 01987654 at on June 15, 2019?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # agent_brokerage_history_list > List the full brokerage career timeline for an agent. List the full brokerage career timeline for an agent. Use to answer where an agent has worked and for how long — for a point-in-time lookup, use [`agent_brokerage_at_date`](/tools/agent-brokerage-history/agent_brokerage_at_date) instead. Returns one entry per (brokerage, period) tuple, oldest first, including current responsible broker (end_date is null for current). Each entry includes brokerage name, DRE license, start/end dates, and a human-readable tenure label like "1 year, 7 months". Sourced from CA DRE public records. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `user_id` | string | No | Platform user UUID. Defaults to the calling user when omitted. | | `dre_license_id` | string | No | CA DRE 8-digit license number (alternative to user_id, useful for non-platform agents on a listing). | ## Example prompts - "Show me my full brokerage career timeline with tenure at each office." - "Pull the brokerage history for the listing agent with DRE license 02114478." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Appointments > Create, search, update, and cancel appointments — including today's schedule and appointment stats. Create, search, update, and cancel appointments — including today's schedule and appointment stats. ## Tools (10) | Tool | Type | What it does | | --- | --- | --- | | [`appointments_create`](/tools/appointments/appointments_create) | Creates data | Create a new appointment, showing, meeting, or follow-up. | | [`appointments_list`](/tools/appointments/appointments_list) | Read-only | Search for appointments by date range, status, or text. | | [`appointments_get`](/tools/appointments/appointments_get) | Read-only | Get full details of a specific appointment by its ID — time, location, type, status, and notes. | | [`appointments_update`](/tools/appointments/appointments_update) | Updates data | Update an existing appointment's details or reschedule it. | | [`appointments_cancel`](/tools/appointments/appointments_cancel) | Irreversible | Cancel an appointment while keeping its record for history. | | [`appointments_today`](/tools/appointments/appointments_today) | Read-only | Get all appointments scheduled for today. | | [`appointments_archive`](/tools/appointments/appointments_archive) | Updates data | Archive an appointment to hide it from active views without deleting it. | | [`appointments_restore`](/tools/appointments/appointments_restore) | Updates data | Restore a previously archived appointment back to active views. | | [`appointments_delete`](/tools/appointments/appointments_delete) | Irreversible | Permanently delete an appointment. | | [`appointments_stats`](/tools/appointments/appointments_stats) | Read-only | Get aggregate statistics for all appointments: counts by status (scheduled/completed/cancelled), upcoming appointments within 7 days, and total counts by type. | --- # appointments_archive > Archive an appointment to hide it from active views without deleting it. Archive an appointment to hide it from active views without deleting it. Archived appointments can be restored later with [`appointments_restore`](/tools/appointments/appointments_restore). Use this for past appointments you want to declutter from your schedule view. Returns the archived appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | Yes | UUID of the appointment to archive. Use [`appointments_list`](/tools/appointments/appointments_list) to find the ID if unknown. | ## Example prompts - "Archive that old showing from last month so it stops cluttering my schedule." - "Hide the cancelled Patterson consultation from my active calendar without deleting it." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # appointments_cancel > Cancel an appointment while keeping its record for history. Cancel an appointment while keeping its record for history. Use when a showing, meeting, or follow-up is called off; to move it to a new time instead, use [`appointments_update`](/tools/appointments/appointments_update). Returns the cancelled appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | Yes | UUID of the appointment to cancel · Format: UUID | | `reason` | string | No | Reason for cancellation | ## Example prompts - "Cancel my 4pm property tour with the Nguyens, they got stuck at work." - "Cancel Thursday's appraisal at 412 Birchwood Ln, but double-check with me before you do it." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. ## Previous name This tool was previously published as `cancel_appointment`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # appointments_create > Create a new appointment, showing, meeting, or follow-up. Create a new appointment, showing, meeting, or follow-up. Use whenever the user wants something on their calendar. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. Returns the created appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | Yes | Title or subject of the appointment | | `appointment_type` | enum | No | Type of appointment · One of: `buyer_consultation`, `lender_prequalification`, `listing_appointment`, `showing`, `property_tour`, `client_meeting`, `inspection`, `appraisal`, `signing`, `final_walkthrough`, `call`, `other`, `open_house`, `escrow_deadline`, `owner_blackout`, `escrow_service` | | `start_time` | string | Yes | Start date and time (ISO 8601 format, e.g., 2025-01-15T14:00:00) | | `end_time` | string | No | End date and time (ISO 8601 format) | | `location` | string | No | Location or address of the appointment | | `notes` | string | No | Notes or description | | `contact_id` | string | No | UUID of the contact this appointment is with | | `listing_id` | string | No | UUID of the listing this appointment is for (if applicable) | ## Example prompts - "Schedule a buyer consultation with the Garcias tomorrow at 2pm at my office." - "Book a listing appointment at 412 Birchwood Ln on Friday at 10am." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_appointment`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # appointments_delete > Permanently delete an appointment. Permanently delete an appointment. This is IRREVERSIBLE - the appointment will be hidden from all views including archived. Only use for test data or duplicate records. For normal cleanup, use [`appointments_archive`](/tools/appointments/appointments_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | Yes | UUID of the appointment to delete. Use [`appointments_get`](/tools/appointments/appointments_get) first to verify. | ## Example prompts - "Permanently delete the duplicate signing appointment I accidentally created twice." - "Delete that test appointment for good, but confirm with me before it's gone forever." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # appointments_get > Get full details of a specific appointment by its ID — time, location, type, status, and notes. Get full details of a specific appointment by its ID — time, location, type, status, and notes. Use after [`appointments_list`](/tools/appointments/appointments_list) when you need every field for one appointment. For multiple appointments, pass appointment_ids instead. Returns the complete appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | No | UUID of a single appointment to retrieve · Format: UUID | | `appointment_ids` | array of strings | No | Array of appointment UUIDs for batch retrieval (max 100). Use either appointment_id OR appointment_ids, not both. · Max items: 100 | ## Example prompts - "Pull up the full details for my 3pm signing appointment." - "What are the notes and location on the Hendersons buyer consultation?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_appointment`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # appointments_list > Search for appointments by date range, status, or text. Search for appointments by date range, status, or text. Use to answer calendar questions or to find an appointment's ID before updating or cancelling it (for today's schedule, [`appointments_today`](/tools/appointments/appointments_today) is faster). Returns IDs only by default for efficiency; use the fields parameter to include additional data. Response includes relatedIds.contactIds for chainable operations. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term — matches against title and location (case-insensitive partial match) | | `start_date` | string | No | Start date to filter from (YYYY-MM-DD) | | `end_date` | string | No | End date to filter to (YYYY-MM-DD) | | `appointment_type` | enum | No | Filter by appointment type · One of: `buyer_consultation`, `lender_prequalification`, `listing_appointment`, `showing`, `property_tour`, `client_meeting`, `inspection`, `appraisal`, `signing`, `final_walkthrough`, `call`, `other`, `open_house`, `escrow_deadline`, `owner_blackout`, `escrow_service` | | `status` | enum | No | Filter by appointment status · One of: `scheduled`, `completed`, `cancelled`, `no_show` | | `fields` | array of enum | No | Field groups to include. Empty/omitted = IDs only. Options: basic (id, title, type, status), schedule (times, location), contacts (contact name), full (all fields). · Values: `basic`, `schedule`, `contacts`, `full` | | `limit` | string | No | Max results. Use a number (e.g. "25") or "all". Default: 10. "all" and any value above 200 are capped at 200 server-side. | ## Example prompts - "What showings do I have on the calendar next week?" - "Find all my scheduled inspections between June 15 and June 30." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `list_appointments`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # appointments_restore > Restore a previously archived appointment back to active views. Restore a previously archived appointment back to active views. Reverses the effect of [`appointments_archive`](/tools/appointments/appointments_archive). Use this when an appointment was archived by mistake. Returns the restored appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | Yes | UUID of the appointment to restore. Must be currently archived. | ## Example prompts - "Restore the Lopez walkthrough I archived by mistake yesterday." - "Bring back that archived inspection appointment for 67 Maple Ct." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # appointments_stats > Get aggregate statistics for all appointments: counts by status (scheduled/completed/cancelled), upcoming appointments within 7 days, and total counts by type. Get aggregate statistics for all appointments: counts by status (scheduled/completed/cancelled), upcoming appointments within 7 days, and total counts by type. Use this for dashboard metrics and scheduling overview. Does NOT return individual appointment records - use [`appointments_list`](/tools/appointments/appointments_list) for that. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How many appointments do I have coming up in the next seven days?" - "Give me a breakdown of my completed versus cancelled appointments this month." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # appointments_today > Get all appointments scheduled for today. Get all appointments scheduled for today. Use when user asks about today's schedule or calendar. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "What does my schedule look like today?" - "Run through everything on my calendar for today before I leave the house." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_today_schedule`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # appointments_update > Update an existing appointment's details or reschedule it. Update an existing appointment's details or reschedule it. Only provided fields are updated — omitted fields remain unchanged. Common uses: move the start/end time, change the location, or fix the title. Returns the updated appointment record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `appointment_id` | string | Yes | UUID of the appointment to update · Format: UUID | | `title` | string | No | Updated title | | `start_time` | string | No | Updated start time (ISO 8601) | | `end_time` | string | No | Updated end time (ISO 8601) | | `location` | string | No | Updated location | | `notes` | string | No | Updated notes | | `status` | enum | No | Updated status · One of: `scheduled`, `completed`, `cancelled`, `no_show` | ## Example prompts - "Move the Garcias showing from Saturday to Sunday at 1pm." - "Mark yesterday's listing appointment at 88 Calloway Dr as completed." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_appointment`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Bulk operations > Bulk archive, restore, and delete across core entities, plus bulk lead status updates. Bulk archive, restore, and delete across core entities, plus bulk lead status updates. ## Tools (19) | Tool | Type | What it does | | --- | --- | --- | | [`bulk_update_lead_status`](/tools/bulk-operations/bulk_update_lead_status) | Bulk changes | Update multiple leads' status at once for bulk lead management. | | [`contacts_batch_archive`](/tools/bulk-operations/contacts_batch_archive) | Bulk changes | Archive multiple contacts at once. | | [`contacts_batch_restore`](/tools/bulk-operations/contacts_batch_restore) | Bulk changes | Restore multiple archived contacts at once. | | [`contacts_batch_delete`](/tools/bulk-operations/contacts_batch_delete) | Bulk changes | Permanently delete multiple contacts that have been archived. | | [`leads_batch_archive`](/tools/bulk-operations/leads_batch_archive) | Bulk changes | Archive multiple leads at once. | | [`leads_batch_restore`](/tools/bulk-operations/leads_batch_restore) | Bulk changes | Restore multiple archived leads at once. | | [`leads_batch_delete`](/tools/bulk-operations/leads_batch_delete) | Bulk changes | Permanently delete multiple leads that have been archived. | | [`clients_batch_archive`](/tools/bulk-operations/clients_batch_archive) | Bulk changes | Archive multiple clients at once. | | [`clients_batch_restore`](/tools/bulk-operations/clients_batch_restore) | Bulk changes | Restore multiple archived clients at once. | | [`clients_batch_delete`](/tools/bulk-operations/clients_batch_delete) | Bulk changes | Permanently delete multiple clients that have been archived. | | [`escrows_batch_archive`](/tools/bulk-operations/escrows_batch_archive) | Bulk changes | Archive multiple escrows at once. | | [`escrows_batch_restore`](/tools/bulk-operations/escrows_batch_restore) | Bulk changes | Restore multiple archived escrows at once. | | [`escrows_batch_delete`](/tools/bulk-operations/escrows_batch_delete) | Bulk changes | Permanently delete multiple escrows that have been archived. | | [`appointments_batch_archive`](/tools/bulk-operations/appointments_batch_archive) | Bulk changes | Archive multiple appointments at once. | | [`appointments_batch_restore`](/tools/bulk-operations/appointments_batch_restore) | Bulk changes | Restore multiple archived appointments at once. | | [`appointments_batch_delete`](/tools/bulk-operations/appointments_batch_delete) | Bulk changes | Permanently delete multiple appointments that have been archived. | | [`listings_batch_archive`](/tools/bulk-operations/listings_batch_archive) | Bulk changes | Archive multiple listings at once. | | [`listings_batch_restore`](/tools/bulk-operations/listings_batch_restore) | Bulk changes | Restore multiple archived listings at once. | | [`listings_batch_delete`](/tools/bulk-operations/listings_batch_delete) | Bulk changes | Permanently delete multiple listings that have been archived. | --- # appointments_batch_archive > Archive multiple appointments at once. Archive multiple appointments at once. Sets appointment status to "cancelled" and marks them as soft-deleted. Returns success and failure counts with IDs. Use [`appointments_list`](/tools/appointments/appointments_list) first to verify the correct appointment IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Appointment UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive all my completed showings from last quarter in one sweep." - "Archive those 12 old appointments, but run the list past me before you do." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # appointments_batch_delete > Permanently delete multiple appointments that have been archived. Permanently delete multiple appointments that have been archived. Appointments must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Appointment UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the archived duplicate appointments from the calendar sync glitch." - "Purge those archived test appointments, but double-check with me before deleting them for good." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # appointments_batch_restore > Restore multiple archived appointments at once. Restore multiple archived appointments at once. Resets appointment status back to "scheduled" and clears the soft-delete timestamp. Returns success and failure counts with IDs. Restored appointments become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Appointment UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the batch of appointments that got archived during the calendar cleanup." - "Unarchive those five meetings from last week, but confirm which ones with me first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # bulk_update_lead_status > Update multiple leads' status at once for bulk lead management. Update multiple leads' status at once for bulk lead management. > [!WARNING] > This affects multiple records permanently. Common use cases: marking old leads as "lost" after cleanup review, updating all contacted leads to "qualified" after qualification calls, batch setting leads to "unqualified" after failed contact attempts. Returns: count of successfully updated leads, any failures with reasons. Always verify the lead_ids list before calling — consider using [`leads_list`](/tools/leads/leads_list) first to confirm you have the right records. Status transitions: new→contacted→qualified→converted (success path) or →unqualified/lost (failure path). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_ids` | array of strings | Yes | Array of lead UUIDs to update. Max 500 per call. Use [`leads_list`](/tools/leads/leads_list) to get IDs. · Max items: 500 · Min items: 1 | | `new_status` | enum | Yes | New status to set for ALL specified leads. converted should only be used via [`leads_convert`](/tools/leads/leads_convert) for proper client creation. · One of: `new`, `contacted`, `qualified`, `unqualified`, `converted`, `lost` | | `notes` | string | No | Notes to append to all updated leads. Example: "Bulk marked lost - no response after 6 months". · Max length: 1000 | ## Example prompts - "Mark all the leads I called yesterday as contacted in one go." - "Set those 30 stale Zillow leads to lost, but show me the list before updating." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # clients_batch_archive > Archive multiple clients at once. Archive multiple clients at once. Clients must be active (not already archived). Returns success and failure counts with IDs. Use [`clients_list`](/tools/clients/clients_list) first to verify the correct client IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Client UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive all the clients whose deals closed back in 2023 to tidy my dashboard." - "Archive these six expired buyer clients, but let me review the names first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # clients_batch_delete > Permanently delete multiple clients that have been archived. Permanently delete multiple clients that have been archived. Clients must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Client UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the archived duplicate client records from the double import." - "Delete those archived test clients for good, but double-check with me before removing anything." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # clients_batch_restore > Restore multiple archived clients at once. Restore multiple archived clients at once. Clients must be currently archived. Returns success and failure counts with IDs. Restored clients become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Client UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the archived clients from the Henderson referral group, they're back in the market." - "Unarchive those three past clients, but confirm exactly who before you restore them." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # contacts_batch_archive > Archive multiple contacts at once. Archive multiple contacts at once. Contacts must be active (not already archived). Returns success and failure counts with IDs. Use [`contacts_list`](/tools/contacts/contacts_list) first to verify the correct contact IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Contact UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive all the vendor contacts from that lender event I never followed up with." - "Archive these 15 outdated contacts, but run the list by me before you do." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # contacts_batch_delete > Permanently delete multiple contacts that have been archived. Permanently delete multiple contacts that have been archived. Contacts must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Contact UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the duplicate contacts we archived after the CSV import mess." - "Wipe those 40 archived test contacts, but double-check with me before deleting anything." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # contacts_batch_restore > Restore multiple archived contacts at once. Restore multiple archived contacts at once. Contacts must be currently archived. Returns success and failure counts with IDs. Restored contacts become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Contact UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the batch of inspector contacts we archived during the spring cleanup." - "Bring back those 20 archived contacts, but confirm the list with me first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # escrows_batch_archive > Archive multiple escrows at once. Archive multiple escrows at once. Escrows must be active (not already archived). Returns success and failure counts with IDs. Use [`escrows_list`](/tools/escrows/escrows_list) first to verify the correct escrow IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Escrow UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive all the escrows that closed before January to clean up my pipeline." - "Archive these four cancelled escrows, but show me the addresses before you do." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # escrows_batch_delete > Permanently delete multiple escrows that have been archived. Permanently delete multiple escrows that have been archived. Escrows must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Escrow UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the archived practice escrows I made during training." - "Delete those duplicate archived escrows, but double-check with me before anything is removed forever." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # escrows_batch_restore > Restore multiple archived escrows at once. Restore multiple archived escrows at once. Escrows must be currently archived. Returns success and failure counts with IDs. Restored escrows become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Escrow UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the two escrows we archived last week, the deals are back on." - "Unarchive that batch of escrows from March, but confirm the list with me first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # leads_batch_archive > Archive multiple leads at once. Archive multiple leads at once. Sets lead status to "archived" and marks them as soft-deleted. Returns success and failure counts with IDs. Use [`leads_list`](/tools/leads/leads_list) first to verify the correct lead IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Lead UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive every lead from the 2024 open house list that never responded." - "Archive those 50 dead sign-call leads, but show me which ones before you do." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # leads_batch_delete > Permanently delete multiple leads that have been archived. Permanently delete multiple leads that have been archived. Leads must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Lead UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the archived spam leads that came through the website form." - "Purge those 80 archived junk leads, but double-check with me before deleting them forever." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # leads_batch_restore > Restore multiple archived leads at once. Restore multiple archived leads at once. Resets lead status back to "new" and clears the soft-delete timestamp. Returns success and failure counts with IDs. Restored leads become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Lead UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the archived leads from the Maple Grove farm, they're active again." - "Unarchive that batch of website leads, but confirm the list with me first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # listings_batch_archive > Archive multiple listings at once. Archive multiple listings at once. Sets listing status to "cancelled" and marks them as archived. Returns success and failure counts with IDs. Use [`listings_list`](/tools/listings/listings_list) first to verify the correct listing IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Listing UUIDs to archive. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Archive all the expired listings from last year so my dashboard only shows active ones." - "Archive these three withdrawn listings, but show me the addresses before you proceed." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # listings_batch_delete > Permanently delete multiple listings that have been archived. Permanently delete multiple listings that have been archived. Listings must be archived before deletion. This action is irreversible. Returns success and failure counts with IDs. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Listing UUIDs to permanently delete. Must be archived. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Permanently delete the archived duplicate listings created by the MLS import error." - "Delete those archived test listings for good, but double-check with me before removing them." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # listings_batch_restore > Restore multiple archived listings at once. Restore multiple archived listings at once. Resets listing status back to "active" and clears the archived flag. Returns success and failure counts with IDs. Restored listings become active again. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Listing UUIDs to restore. Max 100. · Max items: 100 · Min items: 1 | ## Example prompts - "Restore the two listings we archived when the sellers paused, they're relisting now." - "Unarchive that batch of listings from the spring cleanup, but confirm the list first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # Clients > Manage the people you represent — create, update, search, archive, and sync client records. Manage the people you represent — create, update, search, archive, and sync client records. ## Tools (11) | Tool | Type | What it does | | --- | --- | --- | | [`clients_list`](/tools/clients/clients_list) | Read-only | Search for clients. | | [`clients_get`](/tools/clients/clients_get) | Read-only | Get full details of a specific client by ID — contact info, client type, status, representation dates, and tags. | | [`clients_create`](/tools/clients/clients_create) | Creates data | Create a new client (buyer or seller). | | [`clients_update`](/tools/clients/clients_update) | Updates data | Update an existing client's information. | | [`clients_bulk_update`](/tools/clients/clients_bulk_update) | Bulk changes | Update multiple clients at once. | | [`clients_stats`](/tools/clients/clients_stats) | Read-only | Get a summary of all clients including counts by type and status. | | [`clients_archive`](/tools/clients/clients_archive) | Updates data | Archive a client to hide them from active views without deleting their record. | | [`clients_restore`](/tools/clients/clients_restore) | Updates data | Restore a previously archived client back to active views. | | [`clients_find_or_create`](/tools/clients/clients_find_or_create) | Creates data | Look up a client by contact identifier; create one if none exists. | | [`clients_sync_statuses`](/tools/clients/clients_sync_statuses) | Bulk changes | Sync client statuses based on their linked escrow statuses. | | [`clients_delete`](/tools/clients/clients_delete) | Irreversible | Permanently delete a client. | --- # clients_archive > Archive a client to hide them from active views without deleting their record. Archive a client to hide them from active views without deleting their record. Use for past clients you are no longer working with but whose history you want to keep — the client can be restored later with [`clients_restore`](/tools/clients/clients_restore). Returns the archived client record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | UUID of the client to archive · Format: UUID | ## Example prompts - "Archive the Pattersons client record now that they've moved out of state." - "Hide Tom Whitfield from my active client list but keep his history." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `archive_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_bulk_update > Update multiple clients at once. Update multiple clients at once. Requires CLIENT IDs. Use for batch changes like marking every client on a closed escrow as closed. Returns a summary of the clients updated plus any IDs that were not found. ```text WORKFLOW for updating clients from escrows (DIRECT - no intermediate steps): 1. escrows_list → get relatedIds.clientIds (these ARE client IDs!) 2. clients_bulk_update({ client_ids: relatedIds.clientIds, updates: {...} }) ``` > [!NOTE] > [`escrows_list`](/tools/escrows/escrows_list) now returns BOTH: > - relatedIds.clientIds → use directly with this tool > - relatedIds.contactIds → use with [`contacts_get`](/tools/contacts/contacts_get) (contact_ids) (different entity) ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_ids` | array of strings | Yes | Array of CLIENT UUIDs (from [`clients_list`](/tools/clients/clients_list) response.ids). NOT contact IDs from escrows! | | `updates` | object | Yes | Fields to update on all clients | | `updates.status` | enum | No | New status for all clients (active = working with, closed = deal completed, expired = agreement expired, cancelled = client cancelled) · One of: `active`, `closed`, `expired`, `cancelled` | | `updates.client_type` | enum | No | New client type · One of: `buyer`, `seller`, `both` | | `skip_if_already` | boolean | No | If true, skip clients already in target state (default: true). · Default: `true` | ## Example prompts - "Set every client tied to my closed escrows to closed status at once." - "Switch those eight clients to seller type, but show me the list before updating." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. ## Previous name This tool was previously published as `batch_update_clients`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_create > Create a new client (buyer or seller). Create a new client (buyer or seller). Use when someone signs with you or the user asks to add a client directly — to promote an existing lead, use [`leads_convert`](/tools/leads/leads_convert) instead. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. Returns the created client record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | Yes | First name | | `last_name` | string | Yes | Last name | | `email` | string | No | Email address | | `phone` | string | No | Phone number | | `client_type` | enum | Yes | Type of client · One of: `buyer`, `seller`, `both` | | `budget_min` | number | No | Minimum budget (for buyers) | | `budget_max` | number | No | Maximum budget (for buyers) | | `notes` | string | No | Notes about this client | ## Example prompts - "Add the Garcias as new buyer clients with a budget of 550k to 625k." - "Create a seller client for Tom Whitfield, phone 661-555-0182, he's listing his condo." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_delete > Permanently delete a client. Permanently delete a client. This is IRREVERSIBLE - the client will be hidden from all views including archived. Only use for test data or duplicate records. For normal cleanup, use [`clients_archive`](/tools/clients/clients_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | UUID of the client to delete. Use [`clients_get`](/tools/clients/clients_get) first to verify. | ## Example prompts - "Permanently delete that duplicate client record we just archived for Maria Garcia." - "Delete the test client from onboarding for good, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # clients_find_or_create > Look up a client by contact identifier; create one if none exists. Look up a client by contact identifier; create one if none exists. Single-call idempotent path for "promote this contact to a client" intents — prefer this over [`clients_list`](/tools/clients/clients_list) → [`clients_create`](/tools/clients/clients_create) chains. Accepts either contact_id (preferred) or a name/email to resolve against the contact directory first. Returns the client record plus a `created: true|false` flag so the caller can tell whether a new record was inserted. New clients go through the same editable draft flow as [`clients_create`](/tools/clients/clients_create). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | Existing contact UUID (preferred). | | `first_name` | string | No | Given name when resolving by name. | | `last_name` | string | No | Family name when resolving by name. | | `email` | string | No | Contact email for lookup/creation. | | `phone` | string | No | Contact phone for lookup/creation. | | `client_type` | enum | No | Client type to set when creating (default: buyer). · One of: `buyer`, `seller`, `both` | ## Example prompts - "If Marcus Lee from my contacts isn't already a client, set him up as a buyer." - "Promote the contact Priya Shah to a seller client, or pull her file if she exists." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `find_or_create_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_get > Get full details of a specific client by ID — contact info, client type, status, representation dates, and tags. Get full details of a specific client by ID — contact info, client type, status, representation dates, and tags. Use after [`clients_list`](/tools/clients/clients_list) when you need every field for one client. For multiple clients, pass client_ids instead. Returns the complete client record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | No | UUID of a single client to retrieve · Format: UUID | | `client_ids` | array of strings | No | Array of client UUIDs for batch retrieval (max 100). Use either client_id OR client_ids, not both. · Max items: 100 | ## Example prompts - "Pull up everything we have on Maria Garcia's client file." - "Show me the full details for that buyer client you just found." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_list > Search for clients. Search for clients. Returns IDs only by default for efficiency. Use fields parameter to include additional data. Response includes relatedIds.contactIds for chainable operations. > [!NOTE] > When working with escrows, use relatedIds.clientIds directly with [`clients_bulk_update`](/tools/clients/clients_bulk_update). > The contact_ids parameter is for finding clients by their linked contact - only needed for special cases. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term (searches name, email, phone) | | `client_type` | enum | No | Filter by client type · One of: `buyer`, `seller`, `both` | | `status` | enum | No | Filter by client status (active = working with, closed = deal completed, expired = agreement expired, cancelled = client cancelled) · One of: `active`, `closed`, `expired`, `cancelled` | | `contact_ids` | array of strings | No | Filter to clients linked to these contact IDs. CRITICAL for finding clients from escrow contacts - use escrow relatedIds.contactIds here. | | `is_archived` | boolean or string | No | Filter by archived state. Default: false (active clients only). Pass true to find ARCHIVED clients (e.g. for a restore workflow). Pass "any" to include both. | | `fields` | array of enum | No | Field groups to include. Empty/omitted = IDs only. Options: basic (id, type, status), contact (name, email, phone), financial (budget range), activity (dates), full (all fields). · Values: `basic`, `contact`, `financial`, `activity`, `full` | | `limit` | string | No | Max results. Use a number (e.g. "25") or "all". "all" and any value above 200 are capped at 200 server-side. | ## Example prompts - "Show me all my active buyer clients with their contact info." - "Search my clients for anyone named Garcia and pull their phone numbers." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `search_clients`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_restore > Restore a previously archived client back to active views. Restore a previously archived client back to active views. Use when you resume working with a client you had archived. Returns the restored client record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | UUID of the archived client to restore · Format: UUID | ## Example prompts - "Restore the Garcias client file, they're ready to start house hunting again." - "Unarchive that client record for Dana Whitfield I archived last fall." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `restore_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_stats > Get a summary of all clients including counts by type and status. Get a summary of all clients including counts by type and status. Use for overview questions. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How many active buyers versus sellers am I working with right now?" - "Give me a quick summary of my client counts by status." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_clients_summary`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # clients_sync_statuses > Sync client statuses based on their linked escrow statuses. Sync client statuses based on their linked escrow statuses. When escrows close, the clients linked to them should be marked as 'closed'. This tool handles the ENTIRE workflow in ONE call: finds all clients linked to escrows with the specified status who are still marked active, then updates them. ALWAYS use dry_run=true FIRST to preview what would be updated before making actual changes. Returns the list of clients that would be (or were) updated. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `dry_run` | boolean | No | REQUIRED FIRST STEP: Set true to preview changes without making them. Set false to execute the sync. | | `escrow_status` | enum | No | Which escrow status to sync from. closed=successful transactions, cancelled/fell_through=unsuccessful (default: closed) · One of: `closed`, `cancelled`, `fell_through` | ## Example prompts - "Sync my client statuses so everyone whose escrow closed gets marked closed." - "Preview which active clients would flip to closed from finished escrows before making changes." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # clients_update > Update an existing client's information. Update an existing client's information. Only provided fields are updated — omitted fields remain unchanged. Common uses: change status or client type, fix contact details, adjust representation dates. Returns the updated client record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | UUID of the client to update · Format: UUID | | `first_name` | string | No | Updated first name | | `last_name` | string | No | Updated last name | | `email` | string | No | Updated email | | `phone` | string | No | Updated phone | | `status` | enum | No | Updated status (active = working with, closed = deal completed, expired = agreement expired, cancelled = client cancelled, inactive = paused) · One of: `active`, `closed`, `expired`, `cancelled`, `inactive` | | `notes` | string | No | Notes to add | ## Example prompts - "Change Dana Whitfield's phone number to 661-555-0147 on her client record." - "Mark the Hendersons client file as closed now that their purchase recorded." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Commission ledger > Track agent commission payouts per escrow — list, inspect, and update ledger entries from pending through paid. Track agent commission payouts per escrow — list, inspect, and update ledger entries from pending through paid. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`commission_ledger_create`](/tools/commission-ledger/commission_ledger_create) | Creates data | Record a commission ledger entry — the payment-tracking record for a deal commission (status, projected/actual payout dates, check number). | | [`commission_ledger_list`](/tools/commission-ledger/commission_ledger_list) | Read-only | List commission ledger entries visible to the caller, newest first. | | [`commission_ledger_get`](/tools/commission-ledger/commission_ledger_get) | Read-only | Get the full details of a single commission ledger entry by its ID — amounts, splits, status, payout dates, and the linked escrow. | | [`commission_ledger_update`](/tools/commission-ledger/commission_ledger_update) | Updates data | Update mutable fields of a commission ledger entry — typically advancing status (pending → processing → paid), recording the actual payout date / check number, or correcting amounts. | --- # commission_ledger_create > Record a commission ledger entry — the payment-tracking record for a deal commission (status, projected/actual payout dates, check number). Record a commission ledger entry — the payment-tracking record for a deal commission (status, projected/actual payout dates, check number). Use this AFTER [`calculate_commission_split`](/tools/financial/calculate_commission_split) to persist the result so it appears on the agent commission ledger. Requires escrow_id, agent_id, and side. All amounts are optional dollar figures; status defaults to pending. This records a payment to be tracked; it does not move money. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | Escrow/transaction ID this commission belongs to (escrow IDs are human-readable strings, e.g. "ESC-2026-0142", not UUIDs) | | `agent_id` | string | Yes | UUID of the agent (users.id) who earns this commission · Format: UUID | | `side` | enum | Yes | Which side of the deal this commission is for · One of: `listing`, `buyer`, `both` | | `escrow_number` | string | No | Denormalized escrow/file number for list views | | `property_address` | string | No | Denormalized property address for list views | | `transaction_type` | enum | No | Transaction type · One of: `Sale`, `Lease` | | `agent_name` | string | No | Denormalized agent display name | | `sale_price` | number | No | Sale price in dollars · Min: 0 | | `commission_rate` | number | No | Commission rate (percent, e.g. 2.5) · Min: 0 | | `gross_commission` | number | No | Gross commission in dollars · Min: 0 | | `brokerage_split` | number | No | Brokerage split percentage · Min: 0 | | `agent_commission` | number | No | Agent share of the commission in dollars · Min: 0 | | `brokerage_commission` | number | No | Brokerage share of the commission in dollars · Min: 0 | | `referral_fee` | number | No | Referral fee owed in dollars (default 0) · Min: 0 | | `referral_agent` | string | No | Name of the referring agent/company | | `transaction_fee` | number | No | Flat brokerage transaction/admin fee in dollars (default 0) · Min: 0 | | `net_commission` | number | No | Net commission to the agent in dollars · Min: 0 | | `tax_withheld` | boolean | No | Whether tax is withheld on this payout (default false) | | `tax_rate` | number | No | Tax withholding rate percentage (default 0) · Min: 0 | | `status` | enum | No | Payment status (default "pending") · One of: `pending`, `processing`, `paid`, `cancelled` | | `projected_payout_date` | string | No | Projected payout date (YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `notes` | string | No | Free-text notes about this commission | ## Example prompts - "Add a new record under commission ledger." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # commission_ledger_get > Get the full details of a single commission ledger entry by its ID — amounts, splits, status, payout dates, and the linked escrow. Get the full details of a single commission ledger entry by its ID — amounts, splits, status, payout dates, and the linked escrow. Use after [`commission_ledger_list`](/tools/commission-ledger/commission_ledger_list) when you need every field for one entry. Scoped to the caller — returns not-found if the entry belongs to another agent outside the caller scope. Returns the complete ledger entry. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | UUID of the commission ledger entry · Format: UUID | ## Example prompts - "Pull up the details on one of my commission ledger." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # commission_ledger_list > List commission ledger entries visible to the caller, newest first. List commission ledger entries visible to the caller, newest first. Standard agents see only their own entries; brokers/team leads see their brokerage/team. Use to review upcoming or past commission payouts, or to find an entry's ID before updating it. Filter by status, agent, side, and payout-date range. Returns a paginated summary (id, escrow_number, property_address, side, agent_name, amounts, status, payout dates). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Free-text search across escrow number, property address, and agent name | | `status` | enum | No | Filter by payment status · One of: `pending`, `processing`, `paid`, `cancelled` | | `agent_id` | string | No | Filter by agent UUID (users.id) · Format: UUID | | `side` | enum | No | Filter by deal side · One of: `listing`, `buyer`, `both` | | `start_date` | string | No | Only entries with a payout date on/after this date (YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `end_date` | string | No | Only entries with a payout date on/before this date (YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `limit` | number | No | Max results (default 25, cap 100) · Max: 100 · Min: 1 | | `offset` | number | No | Skip N records for pagination (default 0) · Min: 0 | ## Example prompts - "Show me my commission ledger." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # commission_ledger_update > Update mutable fields of a commission ledger entry — typically advancing status (pending → processing → paid), recording the actual payout date / check number, or correcting amounts. Update mutable fields of a commission ledger entry — typically advancing status (pending → processing → paid), recording the actual payout date / check number, or correcting amounts. Use as payouts move through processing. Setting status to paid without an actual_payout_date stamps the current date automatically. Only entries the caller can see may be updated. Returns the updated ledger entry. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | UUID of the commission ledger entry to update · Format: UUID | | `status` | enum | No | New payment status · One of: `pending`, `processing`, `paid`, `cancelled` | | `sale_price` | number | No | Corrected sale price in dollars · Min: 0 | | `commission_rate` | number | No | Corrected commission rate (percent) · Min: 0 | | `brokerage_split` | number | No | Corrected brokerage split percentage · Min: 0 | | `gross_commission` | number | No | Corrected gross commission in dollars · Min: 0 | | `agent_commission` | number | No | Corrected agent commission in dollars · Min: 0 | | `net_commission` | number | No | Corrected net commission in dollars · Min: 0 | | `referral_fee` | number | No | Corrected referral fee in dollars · Min: 0 | | `transaction_fee` | number | No | Corrected transaction fee in dollars · Min: 0 | | `projected_payout_date` | string | No | Projected payout date (YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `actual_payout_date` | string | No | Actual payout date (YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `check_number` | string | No | Check number for the payout | | `deposit_account` | string | No | Deposit account the payout went to | | `notes` | string | No | Free-text notes about this commission | ## Example prompts - "Update one of my commission ledger." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Communication drafts > Draft emails, text messages, and call scripts for your review — nothing is sent until you approve it. Draft emails, text messages, and call scripts for your review — nothing is sent until you approve it. ## Tools (3) | Tool | Type | What it does | | --- | --- | --- | | [`draft_email`](/tools/communication/draft_email) | Draft only | Draft an email for the user to review and send. | | [`draft_sms`](/tools/communication/draft_sms) | Draft only | Draft an SMS text message for the user to review and send. | | [`draft_call_script`](/tools/communication/draft_call_script) | Draft only | Draft a phone call script for the user to reference during a call. | --- # draft_call_script > Draft a phone call script for the user to reference during a call. Draft a phone call script for the user to reference during a call. Use this when the user asks you to prepare talking points, a call script, or help them plan a phone conversation. The script will appear as an interactive card in the chat with a Copy button and a Log Call button. Include a clear opening, key talking points, and suggested close. Tailor the script to real estate contexts: listing presentations, offer discussions, follow-ups, check-ins, etc. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_name` | string | Yes | Full name of the person being called. · Max length: 200 | | `phone` | string | No | Phone number to call. Format: (555) 123-4567 or 5551234567. · Format: Phone number | | `purpose` | string | Yes | Brief purpose of the call. E.g., "Follow up on offer", "Listing presentation", "Check-in after closing". · Max length: 500 | | `script` | string | Yes | The full call script. Include greeting, key discussion points, objection handling, and closing. · Max length: 10000 | | `talking_points` | array of strings | No | Key talking points as a bulleted list. Quick-reference items the agent can glance at during the call. · Max items: 20 | | `contact_id` | string | No | UUID of the contact being called. Used to link the call log after the conversation. · Format: UUID | ## Example prompts - "Prep a call script for my listing presentation with the Whitfields tonight." - "Give me talking points for calling the Garcias about lowering their price to 589k." ## Safety **Draft only.** Prepares a draft for your review — nothing is sent until you approve it. --- # draft_email > Draft an email for the user to review and send. Draft an email for the user to review and send. Use this when the user asks you to compose, write, or draft an email to a contact, lead, client, or any person. The email will appear as an interactive card in the chat where the user can review, edit, and send it via their connected Gmail account. > [!IMPORTANT] > Always look up the recipient's email address first using [`contacts_list`](/tools/contacts/contacts_list) or [`leads_list`](/tools/leads/leads_list) before drafting. Include context from the conversation (property addresses, dates, amounts) to make the email specific and useful. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `to` | string | Yes | Recipient email address. Look up via [`contacts_list`](/tools/contacts/contacts_list) or [`leads_list`](/tools/leads/leads_list) first. · Format: Email address | | `cc` | string | No | CC email address(es), comma-separated if multiple. | | `bcc` | string | No | BCC email address(es), comma-separated if multiple. | | `subject` | string | Yes | Email subject line. Be specific and professional. · Max length: 500 | | `body` | string | Yes | Email body text. Use professional real estate tone. Can include markdown formatting. · Max length: 50000 | | `html_body` | string | No | Optional HTML version of the email body for rich formatting. | | `contact_id` | string | No | UUID of the contact being emailed. Helps link the communication for CRM tracking. · Format: UUID | | `related_entity_type` | enum | No | Type of related entity if this email is about a specific transaction or listing. · One of: `escrow`, `listing`, `appointment`, `deal` | | `related_entity_id` | string | No | UUID of the related entity (escrow, listing, etc.). · Format: UUID | ## Example prompts - "Draft an email to the Garcias confirming Saturday's showing at 412 Birchwood Ln." - "Write an email to my lender contact about the appraisal coming in 15k low." ## Safety **Draft only.** Prepares a draft for your review — nothing is sent until you approve it. --- # draft_sms > Draft an SMS text message for the user to review and send. Draft an SMS text message for the user to review and send. Use this when the user asks you to text, message, or send an SMS to a contact or lead. The message will appear as an interactive card in the chat where the user can review, edit, and send it via Twilio. > [!IMPORTANT] > SMS messages should be concise (under 160 characters for a single segment). Always look up the recipient's phone number first using [`contacts_list`](/tools/contacts/contacts_list) or [`leads_list`](/tools/leads/leads_list). The recipient must have active SMS consent on file before sending. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `to` | string | Yes | Recipient phone number. Look up via [`contacts_list`](/tools/contacts/contacts_list) or [`leads_list`](/tools/leads/leads_list) first. Format: (555) 123-4567 or 5551234567. · Format: Phone number | | `body` | string | Yes | SMS message body. Keep concise — 160 chars = 1 segment. Over 160 splits into multiple segments which cost more. · Max length: 1600 | | `contact_id` | string | No | UUID of the contact being texted. Required for consent verification and CRM tracking. · Format: UUID | | `related_entity_type` | enum | No | Type of related entity if this text is about a specific transaction or listing. · One of: `escrow`, `listing`, `appointment`, `deal` | | `related_entity_id` | string | No | UUID of the related entity (escrow, listing, etc.). · Format: UUID | ## Example prompts - "Text Marcus Lee a quick reminder about tomorrow's 10am walkthrough at 67 Maple Ct." - "Draft a short text to the Hendersons letting them know their offer was accepted." ## Safety **Draft only.** Prepares a draft for your review — nothing is sent until you approve it. --- # Composite workflows > One-call shortcuts that combine several steps, like creating an escrow together with its client records. One-call shortcuts that combine several steps, like creating an escrow together with its client records. ## Tools (3) | Tool | Type | What it does | | --- | --- | --- | | [`create_escrow_with_clients`](/tools/composite/create_escrow_with_clients) | Creates data | Create an escrow AND create/link buyers and sellers in ONE atomic transaction. | | [`log_showing_with_feedback`](/tools/composite/log_showing_with_feedback) | Creates data | Log a property showing AND capture client feedback in ONE call. | | [`create_listing_from_escrow`](/tools/composite/create_listing_from_escrow) | Creates data | Create a new listing pre-populated with data from a closed escrow. | --- # create_escrow_with_clients > Create an escrow AND create/link buyers and sellers in ONE atomic transaction. Create an escrow AND create/link buyers and sellers in ONE atomic transaction. Use this when opening a new transaction where clients don't exist yet — it's equivalent to calling [`lookup_address`](/tools/utility/lookup_address) → [`clients_create`](/tools/clients/clients_create) (×N) → [`escrows_create`](/tools/escrows/escrows_create), but in a single operation that rolls back completely if any step fails. > [!CAUTION] > representation_type determines which parties are YOUR CLIENTS — only add the party you represent. If representation_type='buyer', only add buyers (sellers are just contacts). If 'seller', only add sellers. If 'dual', add both. Returns the created escrow with all linked client IDs. After creation, use [`get_contingency_deadlines`](/tools/deadline/get_contingency_deadlines) to calculate key dates. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `property_address` | string | Yes | Street address (required). Use [`lookup_address`](/tools/utility/lookup_address) first for verified data. | | `display_address` | string | No | Display address if different (e.g., with unit number) | | `city` | string | No | City name (from [`lookup_address`](/tools/utility/lookup_address)) | | `state` | string | No | State abbreviation (e.g., "CA") · Format: Two-letter state code | | `zip_code` | string | No | ZIP code · Format: ZIP code | | `county` | string | No | County name — important for transfer tax calculations | | `latitude` | number | No | Latitude coordinate (from [`lookup_address`](/tools/utility/lookup_address)) | | `longitude` | number | No | Longitude coordinate (from [`lookup_address`](/tools/utility/lookup_address)) | | `purchase_price` | number | Yes | Purchase price in dollars (required) · Min: 0 | | `buyer_commission` | number | No | Buyer agent commission in dollars · Min: 0 | | `buyer_commission_percentage` | number | No | Buyer agent commission as percentage (e.g., 2.5) · Max: 100 · Min: 0 | | `seller_commission` | number | No | Seller agent commission in dollars · Min: 0 | | `seller_commission_percentage` | number | No | Seller agent commission as percentage · Max: 100 · Min: 0 | | `representation_type` | enum | Yes | Who YOU represent. CRITICAL: buyer=only add buyers as clients, seller=only add sellers, dual=add both (required) · One of: `buyer`, `seller`, `dual` | | `acceptance_date` | string | No | Contract acceptance date. Used to calculate contingency deadlines. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `closing_date` | string | No | Expected closing date. Typically 30-45 days from acceptance. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `buyers` | array of objects | No | Array of buyer info. ONLY add if representation_type is "buyer" or "dual". These become YOUR CLIENT records. | | `buyers[].first_name` | string | Yes | Buyer first name (required) | | `buyers[].last_name` | string | Yes | Buyer last name (required) | | `buyers[].email` | string | No | Buyer email · Format: Email address | | `buyers[].phone` | string | No | Buyer phone | | `sellers` | array of objects | No | Array of seller info. ONLY add if representation_type is "seller" or "dual". These become YOUR CLIENT records. | | `sellers[].first_name` | string | Yes | Seller first name (required) | | `sellers[].last_name` | string | Yes | Seller last name (required) | | `sellers[].email` | string | No | Seller email · Format: Email address | | `sellers[].phone` | string | No | Seller phone | ## Example prompts - "Open escrow on 412 Birchwood Ln at 615k, I represent buyers Maria and Luis Garcia." - "Set up a new transaction for 88 Calloway Dr at 745k with my sellers, the Hendersons." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # create_listing_from_escrow > Create a new listing pre-populated with data from a closed escrow. Create a new listing pre-populated with data from a closed escrow. Use when a past client is selling their home (the one they bought through you). Copies property address, coordinates, and links to the seller client automatically. WORKFLOW: For seller representation on a property you previously helped buy, this saves data entry by pulling from the closed escrow. You still need to set listing_price, bedrooms, bathrooms, and other listing-specific fields. Returns the created listing with seller client linked. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the closed escrow to copy address from. Use [`escrows_list`](/tools/escrows/escrows_list) status=closed to find. · Format: UUID | | `seller_client_id` | string | No | UUID of the seller client. Often the buyer from the original escrow. · Format: UUID | | `listing_price` | number | Yes | Listing price in dollars (required) · Min: 0 | | `mls_number` | string | No | MLS number if already listed | | `bedrooms` | number | No | Number of bedrooms · Min: 0 | | `bathrooms` | number | No | Number of bathrooms · Min: 0 | | `square_feet` | number | No | Square footage · Min: 0 | | `property_type` | enum | No | Type of property · One of: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `commercial` | | `description` | string | No | Property description for MLS | | `listing_status` | enum | No | Initial listing status (default: coming_soon) · One of: `coming_soon`, `active` | | `listing_date` | string | No | Date listing goes live (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `expiration_date` | string | No | Listing expiration date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | ## Example prompts - "The Hendersons are selling the home they bought through me, start a listing at 689k." - "Spin up a coming soon listing from the closed escrow on 412 Birchwood Ln at 599k." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # log_showing_with_feedback > Log a property showing AND capture client feedback in ONE call. Log a property showing AND capture client feedback in ONE call. Use after showing a property to a buyer client. Creates a completed appointment record, logs the showing activity, and records feedback (interest level, pros, cons, notes). Equivalent to: [`appointments_create`](/tools/appointments/appointments_create) → [`appointments_update`](/tools/appointments/appointments_update) (status=completed) → [`log_call`](/tools/deadline/log_call) (with feedback notes). Returns the showing record with feedback. Use [`listings_list`](/tools/listings/listings_list) first to get listing_id if unknown. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | UUID of the buyer client being shown the property. Use [`clients_list`](/tools/clients/clients_list) to find. · Format: UUID | | `listing_id` | string | No | UUID of the listing being shown. Use [`listings_list`](/tools/listings/listings_list) to find by address. · Format: UUID | | `property_address` | string | No | Property address (required if no listing_id — for unlisted properties) | | `showing_time` | string | No | When the showing occurred (defaults to now). ISO 8601 format, e.g., 2025-01-22T14:30:00Z. · Format: Date-time (ISO 8601) | | `duration_minutes` | number | No | Duration in minutes (default: 30) · Max: 480 · Min: 5 | | `interest_level` | enum | Yes | Client interest level after viewing (required) · One of: `very_interested`, `interested`, `neutral`, `not_interested`, `rejected` | | `would_offer` | boolean | No | Would the client consider making an offer on this property? | | `pros` | array of strings | No | What the client liked (e.g., ["big backyard", "updated kitchen", "quiet street"]) | | `cons` | array of strings | No | What the client disliked (e.g., ["small bedrooms", "needs roof work", "busy road"]) | | `feedback_notes` | string | No | Detailed feedback or notes from the showing | | `schedule_follow_up` | boolean | No | If true, schedules a follow-up call for next business day | ## Example prompts - "Log the showing at 67 Maple Ct, the Garcias loved the yard but hated the kitchen." - "Record that Marcus toured 230 Sandpiper Way, very interested, and wants to write an offer." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # Contacts > Your wider address book — everyone you know, whether or not they are a client yet. Your wider address book — everyone you know, whether or not they are a client yet. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`contacts_create`](/tools/contacts/contacts_create) | Creates data | Create a new contact in the CRM. | | [`contacts_list`](/tools/contacts/contacts_list) | Read-only | Search for contacts by name, email, phone, or other criteria. | | [`contacts_update`](/tools/contacts/contacts_update) | Updates data | Update an existing contact's information. | | [`contacts_get`](/tools/contacts/contacts_get) | Read-only | Get full details of a specific contact by their ID — names, phones, emails, address, tags, and linked records. | | [`contacts_archive`](/tools/contacts/contacts_archive) | Updates data | Archive a contact to hide them from active views without deleting their record. | | [`contacts_restore`](/tools/contacts/contacts_restore) | Updates data | Restore a previously archived contact back to active views. | | [`contacts_delete`](/tools/contacts/contacts_delete) | Irreversible | Permanently delete a contact. | | [`contacts_stats`](/tools/contacts/contacts_stats) | Read-only | Get aggregate statistics for all contacts: counts by status (homeowner/non_homeowner), counts by role (sphere/lead/client), and total portfolio value. | --- # contacts_archive > Archive a contact to hide them from active views without deleting their record. Archive a contact to hide them from active views without deleting their record. Archived contacts can be restored later with [`contacts_restore`](/tools/contacts/contacts_restore). Use this for contacts you no longer work with but want to keep their history. Returns the archived contact record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | Yes | UUID of the contact to archive. Use [`contacts_list`](/tools/contacts/contacts_list) to find the ID if unknown. | ## Example prompts - "Archive my old stager contact since she retired last month." - "Hide the contractor Bill Hayes from my active contacts but keep his history." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # contacts_create > Create a new contact in the CRM. Create a new contact in the CRM. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. > [!NOTE] > For clients with signed representation agreements, use [`clients_find_or_create`](/tools/clients/clients_find_or_create) instead. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | Yes | First name of the contact | | `last_name` | string | Yes | Last name of the contact | | `email` | string | No | Email address | | `phone` | string | No | Phone number | | `company` | string | No | Company or organization name | | `notes` | string | No | Notes about this contact | ## Example prompts - "Add my new lender contact Sarah Kim from Pacific Trust, phone 661-555-0134." - "Save inspector Dave Romero, dave@romeroinspections.com, to my contact directory." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_contact`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # contacts_delete > Permanently delete a contact. Permanently delete a contact. This is IRREVERSIBLE - the contact will be hidden from all views including archived. Only use for test data or duplicate records. For normal cleanup, use [`contacts_archive`](/tools/contacts/contacts_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | Yes | UUID of the contact to delete. Use [`contacts_get`](/tools/contacts/contacts_get) first to verify. | ## Example prompts - "Permanently delete the duplicate contact card for Sarah Kim that got imported twice." - "Delete that archived test contact for good, but double-check with me before removing it." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # contacts_get > Get full details of a specific contact by their ID — names, phones, emails, address, tags, and linked records. Get full details of a specific contact by their ID — names, phones, emails, address, tags, and linked records. Use after [`contacts_list`](/tools/contacts/contacts_list) when you need every field for one contact. For multiple contacts, pass contact_ids instead. Returns the complete contact record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of a single contact to retrieve · Format: UUID | | `contact_ids` | array of strings | No | Array of contact UUIDs for batch retrieval (max 100). Use either contact_id OR contact_ids, not both. · Max items: 100 | ## Example prompts - "Pull up the full contact card for Dave Romero." - "Show me everything we have on file for my escrow officer Linda Tran." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_contact`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # contacts_list > Search for contacts by name, email, phone, or other criteria. Search for contacts by name, email, phone, or other criteria. Use to find existing contacts in the CRM before creating a new one, or to look up a contact's ID for other tools. Returns matching contacts with their IDs and core contact fields. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term (searches name, email, phone) | | `limit` | number | No | Maximum number of results to return (default 10) | ## Example prompts - "Search my contacts for anyone named Romero." - "Find the phone number for my title rep Sarah Kim." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `search_contacts`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # contacts_restore > Restore a previously archived contact back to active views. Restore a previously archived contact back to active views. Reverses the effect of [`contacts_archive`](/tools/contacts/contacts_archive). Use this when a contact re-engages or was archived by mistake. Returns the restored contact record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | Yes | UUID of the contact to restore. Must be currently archived. | ## Example prompts - "Restore Bill Hayes to my active contacts, he's taking jobs again." - "Unarchive the photographer contact I archived by accident last week." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # contacts_stats > Get aggregate statistics for all contacts: counts by status (homeowner/non_homeowner), counts by role (sphere/lead/client), and total portfolio value. Get aggregate statistics for all contacts: counts by status (homeowner/non_homeowner), counts by role (sphere/lead/client), and total portfolio value. Use this for dashboard metrics and network analysis. Does NOT return individual contact records - use [`contacts_list`](/tools/contacts/contacts_list) for that. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How many homeowners versus non-homeowners are in my contact database?" - "Break down my contacts by sphere, lead, and client and show total portfolio value." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # contacts_update > Update an existing contact's information. Update an existing contact's information. Only provided fields are updated — omitted fields remain unchanged. Common uses: fix a phone number or email, update the address, adjust tags. Returns the updated contact record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | Yes | UUID of the contact to update · Format: UUID | | `first_name` | string | No | New first name | | `last_name` | string | No | New last name | | `email` | string | No | New email address | | `phone` | string | No | New phone number | | `company` | string | No | New company name | | `notes` | string | No | Notes to add or update | ## Example prompts - "Update Dave Romero's phone number to 661-555-0190 in my contacts." - "Change Sarah Kim's company to Golden Valley Lending and add a note she moved firms." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_contact`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Deadlines & transaction tracking > Contingency deadlines, alerts, escrow checklists, call logging, follow-up reminders, and open-house logging. Contingency deadlines, alerts, escrow checklists, call logging, follow-up reminders, and open-house logging. ## Tools (7) | Tool | Type | What it does | | --- | --- | --- | | [`get_contingency_deadlines`](/tools/deadline/get_contingency_deadlines) | Read-only | Calculate contingency dates (inspection, appraisal, loan, closing) for an escrow. | | [`get_deadline_alerts`](/tools/deadline/get_deadline_alerts) | Read-only | Get upcoming deadlines across ALL active escrows. | | [`get_escrow_checklist`](/tools/deadline/get_escrow_checklist) | Read-only | Get a transaction checklist tailored to your representation type (buyer, seller, or dual agency). | | [`log_call`](/tools/deadline/log_call) | Logs activity | Log a phone call with a contact, lead, or escrow party for CRM tracking. | | [`get_follow_up_reminders`](/tools/deadline/get_follow_up_reminders) | Read-only | Get contacts and leads that need follow-up for daily prospecting and client management. | | [`create_open_house`](/tools/deadline/create_open_house) | Creates data | Schedule an open house event for a listing. | | [`log_open_house_visitor`](/tools/deadline/log_open_house_visitor) | Creates data | Log a visitor at an open house to capture their information. | --- # create_open_house > Schedule an open house event for a listing. Schedule an open house event for a listing. Creates a special appointment of type "open_house" linked to the listing for marketing and visitor tracking. Provide EITHER listing_id to link to an existing listing OR property_address for a one-off event. Types: public (general public), broker (agents only), twilight (evening showing), mega (large event with multiple agents). After creating, use [`log_open_house_visitor`](/tools/deadline/log_open_house_visitor) during the event to capture attendee information and automatically create leads. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | No | UUID of the listing this open house is for. Use [`listings_list`](/tools/listings/listings_list) to find ID. · Format: UUID | | `property_address` | string | No | Property address if no listing record exists. One of listing_id or property_address required. | | `start_time` | string | Yes | Start date and time. Examples: "2025-01-25T13:00:00" for 1 PM, "2025-01-25T10:00:00" for 10 AM. (ISO 8601 format, e.g., 2025-01-22T14:30:00Z) · Format: Date-time (ISO 8601) | | `end_time` | string | No | End date and time. Typical duration: 2-4 hours for public, 1-2 hours for broker open. (ISO 8601 format, e.g., 2025-01-22T14:30:00Z) · Format: Date-time (ISO 8601) | | `open_house_type` | enum | No | Type of open house. public=general public (most common), broker=agent preview, twilight=evening showing, mega=large event. · One of: `public`, `broker`, `twilight`, `mega` | | `notes` | string | No | Notes or special instructions. Examples: "Parking in rear", "No shoes inside", "Refreshments provided". · Max length: 2000 | ## Example prompts - "Schedule a public open house at 412 Birchwood Ln this Saturday from 1 to 4." - "Set up a broker preview at 88 Marlowe Ct Tuesday at 10, note parking in rear." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # get_contingency_deadlines > Calculate contingency dates (inspection, appraisal, loan, closing) for an escrow. Calculate contingency dates (inspection, appraisal, loan, closing) for an escrow. ALL ARGUMENTS ARE OPTIONAL. With NO arguments, defaults to the user's most-recent active escrow — call this directly when the user asks "what are my deadlines" or "show me contingency dates" without specifying an escrow. Pass escrow_id to target a specific escrow, OR acceptance_date to compute hypothetically. Day counts default to CAR standards (inspection 17, appraisal 17, loan 21, closing 30); override only if non-standard periods were negotiated. Returns calculated dates only — not legal advice. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `acceptance_date` | string | No | OPTIONAL. Contract acceptance date. If omitted, uses most-recent active escrow's acceptance date. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `escrow_id` | string | No | OPTIONAL. Escrow UUID. If omitted, defaults to most-recent active escrow. Pass to target a specific escrow. · Format: UUID | | `inspection_days` | number | No | Inspection contingency days. CAR default: 17 days. Common alternatives: 10, 14, or waived (0). · Max: 60 · Min: 0 | | `appraisal_days` | number | No | Appraisal contingency days. CAR default: 17 days. Often shortened in competitive markets. · Max: 60 · Min: 0 | | `loan_days` | number | No | Loan contingency days. CAR default: 21 days. Cash deals typically 0. · Max: 60 · Min: 0 | | `closing_days` | number | No | Days from acceptance to close. CAR default: 30 days. Range: 21 (fast) to 60 (complex). · Max: 120 · Min: 0 | ## Example prompts - "What are my contingency deadlines on the 412 Birchwood Ln escrow?" - "If acceptance is May 20 with a 10 day inspection, when do contingencies expire?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_deadline_alerts > Get upcoming deadlines across ALL active escrows. Get upcoming deadlines across ALL active escrows. ALL ARGUMENTS ARE OPTIONAL — call with NO args for the standard daily/weekly review (default: 14 days ahead, includes past due). Use this directly when the user asks "what's due", "deadline alerts", "what's coming up", or "what's past due". Pass escrow_id only to filter to one escrow. Returns contingencies + closing dates with days until due (negative = past due) and urgency. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_ahead` | number | No | How many days ahead to look for upcoming deadlines. Default: 14 days. Use 7 for weekly review, 30 for monthly planning. · Max: 90 · Min: 1 | | `include_past_due` | boolean | No | Include past due deadlines in results. Default: true. Critical for catching missed deadlines. | | `escrow_id` | string | No | Filter to a specific escrow. If not provided, returns deadlines from ALL active escrows. · Format: UUID | ## Example prompts - "What deadlines are coming up across all my escrows in the next two weeks?" - "Anything past due or closing soon that I need to handle this week?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_escrow_checklist > Get a transaction checklist tailored to your representation type (buyer, seller, or dual agency). Get a transaction checklist tailored to your representation type (buyer, seller, or dual agency). Use at contract acceptance to see what's ahead, or mid-transaction to check what's still open. Returns common transaction tasks with suggested due dates calculated from acceptance and closing dates based on standard CAR contract timelines. This is a general reference checklist — not a comprehensive legal compliance tool. Your broker or transaction coordinator can advise on the specific tasks and disclosures required for each transaction. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `representation_type` | enum | No | Who you represent determines the checklist. buyer=BRBC tasks, seller=listing/disclosure tasks, dual=both sets. Default: buyer. · One of: `buyer`, `seller`, `dual` | | `escrow_id` | string | No | Escrow UUID to get dates from. If provided, acceptance_date and closing_date are read from the escrow record. · Format: UUID | | `acceptance_date` | string | No | Contract acceptance date for calculating task due dates. Required if escrow_id not provided. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `closing_date` | string | No | Expected closing date. Required if escrow_id not provided. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | ## Example prompts - "Build me a buyer side checklist for the Garcias escrow on 412 Birchwood Ln." - "We listed 88 Marlowe Ct, acceptance June 3, closing July 6, what seller tasks are due?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_follow_up_reminders > Get contacts and leads that need follow-up for daily prospecting and client management. Get contacts and leads that need follow-up for daily prospecting and client management. Returns people who: (1) have scheduled follow-ups due today or past due, (2) haven't been contacted in X days (configurable). Essential for maintaining relationships and not letting leads go cold. Results are sorted by urgency — past due first, then scheduled follow-ups, then stale contacts. Use this every morning to build your call list. For leads, you can filter by status to focus on new/contacted leads vs qualified ones. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_since_contact` | number | No | Show contacts/leads not contacted in X days. Default: 7 days. Use 14 for less aggressive follow-up. · Max: 365 · Min: 1 | | `include_scheduled` | boolean | No | Include people with scheduled follow-ups due. Default: true. Set false to only see stale contacts. | | `lead_status` | enum | No | Filter leads by status. new=fresh leads, contacted=in touch, qualified=ready to transact, all=all active leads. Default: all. · One of: `new`, `contacted`, `qualified`, `all` | | `limit` | number | No | Maximum results to return. Default: 20. Use higher for comprehensive review. · Max: 100 · Min: 1 | ## Example prompts - "Who needs a follow up today? Build my morning call list." - "Show me leads I have not touched in the last 14 days." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # log_call > Log a phone call with a contact, lead, or escrow party for CRM tracking. Log a phone call with a contact, lead, or escrow party for CRM tracking. > [!IMPORTANT] > This creates a permanent call log record — use this to track ALL phone communications for follow-up history and compliance. Provide EITHER contact_id, lead_id, OR phone_number to identify who was called. Outcomes track the call result for follow-up automation. If outcome is 'callback_requested' or 'scheduled_appointment', the follow_up_date and follow_up_notes are especially important. Call logs appear in the contact/lead activity timeline and can be used for prospecting reports. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of the contact called. Use [`contacts_list`](/tools/contacts/contacts_list) to find ID if unknown. · Format: UUID | | `lead_id` | string | No | UUID of the lead called. Use [`leads_list`](/tools/leads/leads_list) to find ID if unknown. · Format: UUID | | `escrow_id` | string | No | UUID of related escrow if this call was about a specific transaction. · Format: UUID | | `phone_number` | string | No | Phone number called. Use if you do not have a contact_id or lead_id. Format: (555) 123-4567 or 5551234567. · Format: Phone number | | `direction` | enum | No | Call direction. outbound=you called them, inbound=they called you. Default: outbound. · One of: `inbound`, `outbound` | | `outcome` | enum | Yes | Result of the call. Required for proper follow-up tracking. Use connected for live conversations, voicemail for messages left. · One of: `connected`, `voicemail`, `no_answer`, `busy`, `wrong_number`, `callback_requested`, `not_interested`, `scheduled_appointment`, `other` | | `duration_minutes` | number | No | Call duration in minutes. Helps track engagement time for reporting. · Max: 480 · Min: 0 | | `subject` | string | No | Brief subject/reason for call. Examples: "Listing follow-up", "Offer update", "Market check-in". · Max length: 200 | | `notes` | string | No | Detailed notes from the call. What was discussed, action items, client concerns, etc. · Max length: 5000 | | `follow_up_date` | string | No | Date to follow up with this person. Especially important for callback_requested or voicemail outcomes. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `follow_up_notes` | string | No | Specific notes for the follow-up. What to discuss, preparation needed, etc. · Max length: 1000 | At least one of the following is required: `contact_id`, `lead_id`, `phone_number`. ## Example prompts - "Log a 12 minute call with Maria Garcia about the appraisal, she wants a callback Friday." - "Note that I left a voicemail for the lender on the Birchwood escrow this morning." ## Safety **Logs activity.** Adds an activity log entry. Your existing records are not changed. --- # log_open_house_visitor > Log a visitor at an open house to capture their information. Log a visitor at an open house to capture their information. AUTOMATICALLY creates a LEAD record with the open house as the source — this is your primary lead generation tool at open houses. Capture as much information as possible: buyer_status helps prioritize follow-up, working_with_agent indicates if they're represented, price_range helps match them to listings. Link to open_house_id (appointment) or listing_id. After the open house, use [`get_hot_leads`](/tools/reporting/get_hot_leads) to see which visitors should be contacted first based on their engagement score. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | No | UUID of the open house appointment. Links visitor to specific event for tracking. · Format: UUID | | `listing_id` | string | No | UUID of the listing. Alternative if no open_house_id — useful for walk-in visitors. · Format: UUID | | `first_name` | string | Yes | Visitor first name. Required for lead creation. · Max length: 100 · Min length: 1 | | `last_name` | string | Yes | Visitor last name. Required for lead creation. · Max length: 100 · Min length: 1 | | `email` | string | No | Visitor email. Critical for follow-up — try to get this! · Format: Email address | | `phone` | string | No | Visitor phone. Important for quick follow-up calls. · Format: Phone number | | `buyer_status` | enum | No | Buyer readiness. pre_approved and ready_to_buy are highest priority. just_looking may be long-term nurture. · One of: `just_looking`, `starting_search`, `actively_searching`, `ready_to_buy`, `pre_approved` | | `working_with_agent` | boolean | No | Already working with another agent? If true, be mindful of ethics rules. | | `interested_in_property` | boolean | No | Interested in THIS specific property? Helps gauge if they are a buyer lead or just curious. | | `price_range_min` | number | No | Minimum price range. Helps match to other listings. · Min: 0 | | `price_range_max` | number | No | Maximum price range. Helps match to other listings. · Min: 0 | | `interest_level` | enum | No | Overall interest level in the property. Helps prioritize follow-up after the open house. · One of: `low`, `medium`, `high` | | `notes` | string | No | Additional notes. Feedback on property, what they are looking for, timeline, etc. · Max length: 2000 | At least one of the following is required: `email`, `phone`. ## Example prompts - "Log visitor Dana Whitfield from today's open house, pre approved and shopping 600k to 700k." - "Add visitor Tom Nguyen from the Birchwood open house, he already has an agent." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # Deals (vendor pipeline) > Track vendor deals through the vendor sales pipeline — create, update, stats, and lifecycle operations. Track vendor deals through the vendor sales pipeline — create, update, stats, and lifecycle operations. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`deals_list`](/tools/deals/deals_list) | Read-only | Search for vendor deals by title, status, vendor type, or deal type. | | [`deals_get`](/tools/deals/deals_get) | Read-only | Get full details of a vendor deal by UUID, including contact info, vendor type, deal type, financial details (amount, fee, commission), linked partner, linked escrow, property address, notes, and tags. | | [`deals_create`](/tools/deals/deals_create) | Creates data | Create a new vendor deal to track a transaction or payment with a vendor partner. | | [`deals_update`](/tools/deals/deals_update) | Updates data | Update an existing vendor deal's information or status. | | [`deals_stats`](/tools/deals/deals_stats) | Read-only | Get aggregate statistics for vendor deals: total count, counts by status, counts by payment status, total revenue, total fees, total commissions, counts by vendor type. | | [`deals_archive`](/tools/deals/deals_archive) | Updates data | Archive a vendor deal to hide it from active views without deleting it. | | [`deals_restore`](/tools/deals/deals_restore) | Updates data | Restore a previously archived vendor deal back to active views. | | [`deals_delete`](/tools/deals/deals_delete) | Irreversible | Permanently delete a vendor deal. | --- # deals_archive > Archive a vendor deal to hide it from active views without deleting it. Archive a vendor deal to hide it from active views without deleting it. Archived deals can be restored later with [`deals_restore`](/tools/deals/deals_restore). Use this for completed or cancelled deals you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `deal_id` | string | Yes | UUID of the deal to archive. Use [`deals_list`](/tools/deals/deals_list) to find. · Format: UUID | ## Example prompts - "Archive the cancelled photography deal at 17 Quail Run so it stops cluttering my dashboard." - "Move the paid Birchwood inspection deal out of my active pipeline, keep it recoverable." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # deals_create > Create a new vendor deal to track a transaction or payment with a vendor partner. Create a new vendor deal to track a transaction or payment with a vendor partner. WORKFLOW: 1) Optionally use [`contacts_list`](/tools/contacts/contacts_list) to find the contact, [`partners_list`](/tools/partners/partners_list) to find the partner, and [`escrows_list`](/tools/escrows/escrows_list) to find the escrow 2) Create the deal with vendor_type, deal details, and financial info. Returns the created deal with ID and display_id. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of an existing contact for this deal. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | Yes | Type of vendor (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `partner_id` | string | No | UUID of the linked partner. Use [`partners_list`](/tools/partners/partners_list) to find. · Format: UUID | | `escrow_service_id` | string | No | UUID of the linked escrow service record. · Format: UUID | | `escrow_id` | string | No | UUID of the linked escrow. Use [`escrows_list`](/tools/escrows/escrows_list) to find. · Format: UUID | | `title` | string | No | Title or description for this deal (e.g. "Home Inspection - 123 Main St") | | `deal_type` | string | No | Type of deal (e.g. inspection, appraisal, escrow_service, title_service, photography, staging, repair) | | `amount` | number | No | Total deal amount in dollars · Min: 0 | | `fee` | number | No | Service fee amount in dollars · Min: 0 | | `commission_amount` | number | No | Commission or referral amount in dollars · Min: 0 | | `property_address` | string | No | Property address associated with this deal | | `notes` | string | No | Additional notes about the deal | | `tags` | array of strings | No | Tags for categorization (e.g. ["rush", "repeat-client", "discounted"]) | ## Example prompts - "Create a new deal for a home inspection at 412 Birchwood Ln, 650 dollars." - "Track a staging job for the Garcias listing, 2400 total with a 200 referral fee." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # deals_delete > Permanently delete a vendor deal. Permanently delete a vendor deal. This is IRREVERSIBLE - the deal will be hidden from all views including archived. The deal must be archived first. For normal cleanup, use [`deals_archive`](/tools/deals/deals_archive) instead, which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `deal_id` | string | Yes | UUID of the deal to delete. Must be archived first. Use [`deals_get`](/tools/deals/deals_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate inspection deal we created twice for 412 Birchwood Ln." - "Erase that test deal completely, but double-check with me before deleting anything." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # deals_get > Get full details of a vendor deal by UUID, including contact info, vendor type, deal type, financial details (amount, fee, commission), linked partner, linked escrow, property address, notes, and tags. Get full details of a vendor deal by UUID, including contact info, vendor type, deal type, financial details (amount, fee, commission), linked partner, linked escrow, property address, notes, and tags. Returns all fields. If you don't have the UUID, use [`deals_list`](/tools/deals/deals_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `deal_id` | string | Yes | UUID of the deal. Use [`deals_list`](/tools/deals/deals_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up the full details on the 412 Birchwood Ln inspection deal." - "What is the fee and payment status on the Marlowe Ct appraisal deal?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # deals_list > Search for vendor deals by title, status, vendor type, or deal type. Search for vendor deals by title, status, vendor type, or deal type. Returns pagination info (total_count, has_more, offset, limit). Results include: id, display_id, title, vendor_type, deal_type, status, payment_status, amount, fee, commission_amount, property_address, contact info, created_at. Filter by status (pending/in_progress/completed/invoiced/paid/cancelled/disputed), payment_status (unpaid/invoiced/paid/refunded), vendor_type, or deal_type. For a specific deal by ID, use [`deals_get`](/tools/deals/deals_get) instead. For aggregate stats, use [`deals_stats`](/tools/deals/deals_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against title, property_address, notes (case-insensitive partial match) | | `status` | enum | No | Filter by status. pending=awaiting start, in_progress=work underway, completed=work done, invoiced=invoice sent, paid=payment received, cancelled=deal off, disputed=payment issue (default: all) · One of: `all`, `pending`, `in_progress`, `completed`, `invoiced`, `paid`, `cancelled`, `disputed` | | `payment_status` | enum | No | Filter by payment status (default: all) · One of: `all`, `unpaid`, `invoiced`, `paid`, `refunded` | | `vendor_type` | string | No | Filter by vendor type (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `deal_type` | string | No | Filter by deal type (e.g. inspection, appraisal, escrow_service, title_service, photography, staging, repair) | | `vertical` | string | No | Filter by industry vertical (e.g., lending, insurance, inspection) | | `date_from` | string | No | Show deals created on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show deals created on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived deals (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Show me all my unpaid inspection deals from the last 30 days." - "Pull up every invoiced deal that has not been paid yet." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # deals_restore > Restore a previously archived vendor deal back to active views. Restore a previously archived vendor deal back to active views. Reverses the effect of [`deals_archive`](/tools/deals/deals_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `deal_id` | string | Yes | UUID of the deal to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Bring back the archived appraisal deal for 88 Marlowe Ct, the client reopened it." - "Restore the Quail Run photography deal I archived last week." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # deals_stats > Get aggregate statistics for vendor deals: total count, counts by status, counts by payment status, total revenue, total fees, total commissions, counts by vendor type. Get aggregate statistics for vendor deals: total count, counts by status, counts by payment status, total revenue, total fees, total commissions, counts by vendor type. Use this for dashboard metrics and financial summaries. Does NOT return individual records - use [`deals_list`](/tools/deals/deals_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "Give me this month's deal totals, revenue, fees, and commissions by vendor type." - "How many of my deals are still unpaid versus paid this quarter?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # deals_update > Update an existing vendor deal's information or status. ```text Update an existing vendor deal's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: status change (pending->in_progress->completed->invoiced->paid), update financial details, link to escrow. Returns the updated deal record. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `deal_id` | string | Yes | UUID of the deal to update (required). Use [`deals_list`](/tools/deals/deals_list) to find. · Format: UUID | | `contact_id` | string | No | Updated contact ID. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | No | Updated vendor type | | `partner_id` | string | No | Updated partner ID. Use [`partners_list`](/tools/partners/partners_list) to find. · Format: UUID | | `escrow_service_id` | string | No | Updated escrow service record ID. · Format: UUID | | `escrow_id` | string | No | Updated escrow ID. Use [`escrows_list`](/tools/escrows/escrows_list) to find. · Format: UUID | | `title` | string | No | Updated title | | `deal_type` | string | No | Updated deal type | | `status` | enum | No | Updated status · One of: `pending`, `in_progress`, `completed`, `invoiced`, `paid`, `cancelled`, `disputed` | | `payment_status` | enum | No | Updated payment status · One of: `unpaid`, `invoiced`, `paid`, `refunded` | | `amount` | number | No | Updated deal amount · Min: 0 | | `fee` | number | No | Updated service fee · Min: 0 | | `commission_amount` | number | No | Updated commission amount · Min: 0 | | `property_address` | string | No | Updated property address | | `notes` | string | No | Updated notes | | `tags` | array of strings | No | Updated tags array (replaces existing tags) | | `care_status` | enum | No | Care status for tracking. Setting this also updates care_status_updated_at and care_status_updated_by audit columns. · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `care_status_note` | string | No | Note explaining the care status change (audit trail). | | `started_at` | string | No | Timestamp the deal work started. Usually set alongside status=in_progress. (ISO 8601 format, e.g., 2025-01-22T14:30:00Z) · Format: Date-time (ISO 8601) | | `completed_at` | string | No | Timestamp the deal was completed. Usually set alongside status=completed. (ISO 8601 format, e.g., 2025-01-22T14:30:00Z) · Format: Date-time (ISO 8601) | | `paid_date` | string | No | Date the deal was paid out. Usually set alongside payment_status=paid. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `record_data` | object (free-form) | No | Pipeline-specific vertical data as a JSON object. Replaces (does not merge with) the existing record_data. | ## Example prompts - "Mark the Birchwood Ln inspection deal as completed and invoiced." - "The Marlowe Ct appraisal got paid today, update its payment status and paid date." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Documents > Read AI risk-analysis results for documents uploaded to a transaction. Read AI risk-analysis results for documents uploaded to a transaction. ## Tools (2) | Tool | Type | What it does | | --- | --- | --- | | [`documents_get_risks`](/tools/documents/documents_get_risks) | Read-only | Get the latest AI risk analysis for a document the agent owns. | | [`documents_list`](/tools/documents/documents_list) | Read-only | List the documents attached to a specific entity (escrow, listing, client, lead, or appointment). | --- # documents_get_risks > Get the latest AI risk analysis for a document the agent owns. Get the latest AI risk analysis for a document the agent owns. Returns findings (each with id, severity P0/P1/P2, title, description, citation, recommendation), summary counts (p0_count, p1_count, p2_count, overall_severity, headline), and the analysis state (pending / in_review / acknowledged / escalated / resolved). Returns null if no analysis has been run on this document yet — the agent must trigger analysis from the web app first (Risk Analysis modal on the document detail page). The findings are AI-generated risk signals, not legal advice; always defer to the agent's broker or attorney for actual decisions. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `document_id` | string | Yes | The document_id (e.g. "doc_7022796200ff"). Get this from the documents tool or from an escrow's document list. · Max length: 50 · Min length: 1 | ## Example prompts - "Any red flags in the risk analysis on the Birchwood purchase agreement?" - "Show me the P0 findings from the AI review of the Garcias disclosure packet." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # documents_list > List the documents attached to a specific entity (escrow, listing, client, lead, or appointment). List the documents attached to a specific entity (escrow, listing, client, lead, or appointment). Returns each document id (pass it to [`documents_get_risks`](/tools/documents/documents_get_risks)), filename, original_name, document_type, category, mime_type, file_size, requires_signature, signed_at, is_primary_contract, and created_at, newest first. Use this to see what is on file for a transaction (the RPA, TDS, NHD, pre-approval on an escrow). Read-only; returns only documents the agent can access under their tenant scope. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `entity_type` | enum | Yes | The kind of record the documents are attached to. · One of: `escrow`, `listing`, `client`, `lead`, `appointment` | | `entity_id` | string | Yes | The id of the escrow, listing, client, lead, or appointment. · Max length: 50 · Min length: 1 | | `limit` | number | No | Max documents to return (default 50). · Max: 200 · Min: 1 | ## Example prompts - "Show me my documents." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # DRE license lookup > Look up a California DRE real-estate license via the official DRE API. Look up a California DRE real-estate license via the official DRE API. ## Tools (1) | Tool | Type | What it does | | --- | --- | --- | | [`lookup_dre_license`](/tools/dre-license/lookup_dre_license) | External lookup | Look up a California DRE (Department of Real Estate) license by ID. | --- # lookup_dre_license > Look up a California DRE (Department of Real Estate) license by ID. Look up a California DRE (Department of Real Estate) license by ID. Returns comprehensive license information: license type (salesperson/broker/corporation), licensee name, license status (active/inactive/expired/cancelled), expiration date, business and mailing addresses, and for salespersons: their responsible broker information. Works for all license types: salesperson (starts with 01 or 02), broker (01 or 02), corporation (01). Use this to verify agents you are working with or to look up cooperating brokers. Note: This makes an external API call to the DRE — expect a 1-3 second response time. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `license_id` | string | Yes | The 8-digit DRE license ID. Examples: "02203217" (salesperson), "01910265" (corporation), "00123456" (broker). Leading zeros required. | ## Example prompts - "Verify DRE license 02203217 for the buyers agent writing the Birchwood offer." - "Check whether license 01910265 is still active and when it expires." ## Safety **External lookup.** A read-only lookup against a service outside ActuallyCare. It may take a few seconds and is subject to that service’s availability. --- # Engagements (vendor pipeline) > Track vendor engagements and interactions — create, update, stats, and lifecycle operations. Track vendor engagements and interactions — create, update, stats, and lifecycle operations. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`engagements_list`](/tools/engagements/engagements_list) | Read-only | Search for vendor engagements by title, status, vendor type, or engagement type. | | [`engagements_get`](/tools/engagements/engagements_get) | Read-only | Get full details of a vendor engagement by UUID, including contact info, vendor type, engagement type, schedule, location, meeting URL, notes, tags, and linked prospect. | | [`engagements_create`](/tools/engagements/engagements_create) | Creates data | Create a new vendor engagement to track a meeting, call, or event with a vendor. | | [`engagements_update`](/tools/engagements/engagements_update) | Updates data | Update an existing vendor engagement's information or status. | | [`engagements_stats`](/tools/engagements/engagements_stats) | Read-only | Get aggregate statistics for vendor engagements: total count, counts by status, counts by vendor type, counts by engagement type, upcoming engagements count. | | [`engagements_archive`](/tools/engagements/engagements_archive) | Updates data | Archive a vendor engagement to hide it from active views without deleting it. | | [`engagements_restore`](/tools/engagements/engagements_restore) | Updates data | Restore a previously archived vendor engagement back to active views. | | [`engagements_delete`](/tools/engagements/engagements_delete) | Irreversible | Permanently delete a vendor engagement. | --- # engagements_archive > Archive a vendor engagement to hide it from active views without deleting it. Archive a vendor engagement to hide it from active views without deleting it. Archived engagements can be restored later with [`engagements_restore`](/tools/engagements/engagements_restore). Use this for completed or cancelled engagements you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `engagement_id` | string | Yes | UUID of the engagement to archive. Use [`engagements_list`](/tools/engagements/engagements_list) to find. · Format: UUID | ## Example prompts - "Archive the cancelled March networking event so it is off my dashboard." - "Clear that completed office visit with Pacific Escrow out of my active list." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # engagements_create > Create a new vendor engagement to track a meeting, call, or event with a vendor. Create a new vendor engagement to track a meeting, call, or event with a vendor. WORKFLOW: 1) Optionally use [`contacts_list`](/tools/contacts/contacts_list) to find the contact and [`prospects_list`](/tools/prospects/prospects_list) to find the prospect 2) Create the engagement with vendor_type, type, and schedule details. Returns the created engagement with ID and display_id. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of an existing contact for this engagement. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | Yes | Type of vendor (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `prospect_id` | string | No | UUID of the linked prospect. Use [`prospects_list`](/tools/prospects/prospects_list) to find. · Format: UUID | | `title` | string | No | Title or description for this engagement (e.g. "Coffee with John - Title Rep") | | `engagement_type` | string | No | Type of engagement (e.g. coffee_meeting, lunch, phone_call, office_visit, networking_event, training, co_marketing) | | `scheduled_date` | string | No | Date of the engagement (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `scheduled_time` | string | No | Time of the engagement (e.g. "14:30", "2:30 PM") | | `duration_minutes` | number | No | Expected duration in minutes (e.g. 30, 60, 90) · Min: 1 | | `location` | string | No | Physical location or address for the engagement | | `meeting_url` | string | No | Virtual meeting URL (Zoom, Google Meet, etc.) | | `notes` | string | No | Additional notes or agenda items for the engagement | | `tags` | array of strings | No | Tags for categorization (e.g. ["first-meeting", "follow-up", "co-marketing"]) | ## Example prompts - "Set up a coffee meeting with lender Marcus Hale next Tuesday at 9 at Brew House." - "Schedule a 60 minute Zoom training with the inspector team Friday at 2." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # engagements_delete > Permanently delete a vendor engagement. Permanently delete a vendor engagement. This is IRREVERSIBLE - the engagement will be hidden from all views including archived. The engagement must be archived first. For normal cleanup, use [`engagements_archive`](/tools/engagements/engagements_archive) instead, which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `engagement_id` | string | Yes | UUID of the engagement to delete. Must be archived first. Use [`engagements_get`](/tools/engagements/engagements_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate lunch entry I accidentally logged twice for Marcus Hale." - "Wipe that test engagement entirely, but double-check with me before deleting it." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # engagements_get > Get full details of a vendor engagement by UUID, including contact info, vendor type, engagement type, schedule, location, meeting URL, notes, tags, and linked prospect. Get full details of a vendor engagement by UUID, including contact info, vendor type, engagement type, schedule, location, meeting URL, notes, tags, and linked prospect. Returns all fields. If you don't have the UUID, use [`engagements_list`](/tools/engagements/engagements_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `engagement_id` | string | Yes | UUID of the engagement. Use [`engagements_list`](/tools/engagements/engagements_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up the details for my Thursday lunch with the title rep." - "What is the meeting link and agenda for my training with the lender team Friday?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # engagements_list > Search for vendor engagements by title, status, vendor type, or engagement type. Search for vendor engagements by title, status, vendor type, or engagement type. Returns pagination info (total_count, has_more, offset, limit). Results include: id, display_id, title, vendor_type, engagement_type, status, scheduled_date, scheduled_time, duration_minutes, location, contact info, created_at. Filter by status (proposed/scheduled/in_progress/follow_up/completed/cancelled), vendor_type, or engagement_type. For a specific engagement by ID, use [`engagements_get`](/tools/engagements/engagements_get) instead. For aggregate stats, use [`engagements_stats`](/tools/engagements/engagements_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against title, notes (case-insensitive partial match) | | `status` | enum | No | Filter by status. proposed=suggested, scheduled=confirmed, in_progress=happening now, follow_up=needs follow-up, completed=done, cancelled=not happening (default: all) · One of: `all`, `proposed`, `scheduled`, `in_progress`, `follow_up`, `completed`, `cancelled` | | `vendor_type` | string | No | Filter by vendor type (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `engagement_type` | string | No | Filter by engagement type (e.g. coffee_meeting, lunch, phone_call, office_visit, networking_event, training, co_marketing) | | `date_from` | string | No | Show engagements scheduled on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show engagements scheduled on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived engagements (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "What meetings do I have scheduled with lenders this month?" - "List every coffee meeting still sitting in proposed status that needs confirming." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # engagements_restore > Restore a previously archived vendor engagement back to active views. Restore a previously archived vendor engagement back to active views. Reverses the effect of [`engagements_archive`](/tools/engagements/engagements_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `engagement_id` | string | Yes | UUID of the engagement to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived coffee meeting with Marcus Hale, we are rescheduling it." - "Bring back the training engagement I archived by mistake yesterday." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # engagements_stats > Get aggregate statistics for vendor engagements: total count, counts by status, counts by vendor type, counts by engagement type, upcoming engagements count. Get aggregate statistics for vendor engagements: total count, counts by status, counts by vendor type, counts by engagement type, upcoming engagements count. Use this for dashboard metrics. Does NOT return individual records - use [`engagements_list`](/tools/engagements/engagements_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How many vendor engagements did I complete this month, broken down by type?" - "How many upcoming meetings do I have on the books versus cancelled this month?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # engagements_update > Update an existing vendor engagement's information or status. ```text Update an existing vendor engagement's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: status change (proposed->scheduled->in_progress->completed), reschedule, add follow-up notes. Returns the updated engagement record. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `engagement_id` | string | Yes | UUID of the engagement to update (required). Use [`engagements_list`](/tools/engagements/engagements_list) to find. · Format: UUID | | `contact_id` | string | No | Updated contact ID. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | No | Updated vendor type | | `prospect_id` | string | No | Updated prospect ID. Use [`prospects_list`](/tools/prospects/prospects_list) to find. · Format: UUID | | `title` | string | No | Updated title | | `engagement_type` | string | No | Updated engagement type | | `status` | enum | No | Updated status · One of: `proposed`, `scheduled`, `in_progress`, `follow_up`, `completed`, `cancelled` | | `scheduled_date` | string | No | Updated scheduled date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `scheduled_time` | string | No | Updated scheduled time | | `duration_minutes` | number | No | Updated duration in minutes · Min: 1 | | `location` | string | No | Updated location | | `meeting_url` | string | No | Updated meeting URL | | `notes` | string | No | Updated notes | | `tags` | array of strings | No | Updated tags array (replaces existing tags) | | `care_status` | enum | No | Care status for tracking. Setting this also updates care_status_updated_at and care_status_updated_by audit columns. · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `care_status_note` | string | No | Note explaining the care status change (audit trail). | | `completed_at` | string | No | Timestamp the engagement was completed. Usually set alongside status=completed. (ISO 8601 format, e.g., 2025-01-22T14:30:00Z) · Format: Date-time (ISO 8601) | | `record_data` | object (free-form) | No | Pipeline-specific vertical data as a JSON object. Replaces (does not merge with) the existing record_data. | ## Example prompts - "Push my lunch with the title rep to next Wednesday at noon." - "Mark Tuesday's coffee with Marcus Hale completed and note he wants co-marketing flyers." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Entity links > Create, list, and remove relationships between any two records — link contacts to escrows, leads to listings, and more. Create, list, and remove relationships between any two records — link contacts to escrows, leads to listings, and more. ## Tools (3) | Tool | Type | What it does | | --- | --- | --- | | [`entity_links_list`](/tools/entity-links/entity_links_list) | Read-only | List entity links for a specific entity. | | [`entity_links_create`](/tools/entity-links/entity_links_create) | Creates data | Create a cross-vertical link between two entities (e.g., link a lending deal to a real estate escrow). | | [`entity_links_delete`](/tools/entity-links/entity_links_delete) | Irreversible | Remove a cross-vertical entity link by its ID. | --- # entity_links_create > Create a cross-vertical link between two entities (e.g., link a lending deal to a real estate escrow). Create a cross-vertical link between two entities (e.g., link a lending deal to a real estate escrow). Both entities must exist. The link is queryable from either side. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `source_type` | enum | Yes | Source entity type · One of: `escrow`, `deal`, `lead`, `client`, `appointment`, `listing` | | `source_id` | string | Yes | Source entity ID | | `source_vertical` | string | Yes | Source entity vertical (e.g., real_estate, lending) | | `target_type` | enum | Yes | Target entity type · One of: `escrow`, `deal`, `lead`, `client`, `appointment`, `listing` | | `target_id` | string | Yes | Target entity ID | | `target_vertical` | string | Yes | Target entity vertical | | `link_type` | enum | No | Type of relationship (default: related) · One of: `related`, `converted_from`, `depends_on`, `parent_of`, `assigned_to` | | `notes` | string | No | Optional notes about the relationship | ## Example prompts - "Link the Hale lending deal to the Birchwood Ln escrow so they cross-reference." - "Connect the Marlowe Ct inspection deal to its escrow, note they share the same client." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # entity_links_delete > Remove a cross-vertical entity link by its ID. Remove a cross-vertical entity link by its ID. Use when two records were linked by mistake or the relationship no longer applies — the linked records themselves are not touched. Find the link ID with [`entity_links_list`](/tools/entity-links/entity_links_list) first. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | UUID of the entity link to remove · Format: UUID | ## Example prompts - "Unlink the Hale lending deal from the old Birchwood escrow, it points to the wrong file." - "Remove the bad link between those two records, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # entity_links_list > List entity links for a specific entity. List entity links for a specific entity. Query by entity_type + entity_id to find all related entities across verticals — for example all deals linked to an escrow, or all escrows linked to a deal. Use before creating a link (to avoid duplicates) or to navigate between related records. Returns the matching links with both endpoints and each link's ID. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term (not used for entity links — use entity_type + entity_id instead) | | `entity_type` | enum | No | Type of entity to find links for · One of: `escrow`, `deal`, `lead`, `client`, `appointment`, `listing` | | `entity_id` | string | No | ID of the entity to find links for | | `source_type` | string | No | Filter by source entity type | | `source_id` | string | No | Filter by source entity ID | ## Example prompts - "What deals are linked to the escrow at 412 Birchwood Ln?" - "Show me every record connected to the Hale lending deal across verticals." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Escrows > Your transactions from contract to close — create, search, update, and manage escrows. Your transactions from contract to close — create, search, update, and manage escrows. ## Tools (9) | Tool | Type | What it does | | --- | --- | --- | | [`escrows_list`](/tools/escrows/escrows_list) | Read-only | Search for escrows. | | [`escrows_get`](/tools/escrows/escrows_get) | Read-only | Get full details of a specific escrow by ID — property, parties, key dates, price, commission, and status. | | [`escrows_create`](/tools/escrows/escrows_create) | Creates data | Create a new escrow/transaction record. | | [`escrows_update`](/tools/escrows/escrows_update) | Updates data | Update an existing escrow/transaction record. | | [`escrows_stats`](/tools/escrows/escrows_stats) | Read-only | Get a summary of all escrows including counts by status and upcoming closings. | | [`escrow_parties_sync`](/tools/escrows/escrow_parties_sync) | Updates data | Synchronize the buyer and/or seller client rosters for an escrow. | | [`escrows_archive`](/tools/escrows/escrows_archive) | Updates data | Archive an escrow to hide it from active views without deleting it. | | [`escrows_restore`](/tools/escrows/escrows_restore) | Updates data | Restore a previously archived escrow back to active views. | | [`escrows_delete`](/tools/escrows/escrows_delete) | Irreversible | Permanently delete an escrow. | --- # escrow_parties_sync > Synchronize the buyer and/or seller client rosters for an escrow. ```text Synchronize the buyer and/or seller client rosters for an escrow. Pass the full desired roster for each side; the tool compares it against the escrow's current parties and adds/removes as needed. Provide only the sides you want to change — omit a side to leave it unchanged, pass an empty array to remove all parties on that side. Each client_id must already exist (use clients_create or clients_find_or_create first). Returns { added, removed, buyers, sellers } summarizing the change and the resulting rosters. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow whose roster you want to sync. | | `buyers` | array of strings | No | Desired buyer client UUIDs (full set — compared against the current roster). Omit to leave buyers unchanged; pass [] to clear. | | `sellers` | array of strings | No | Desired seller client UUIDs (full set — compared against the current roster). Omit to leave sellers unchanged; pass [] to clear. | ## Example prompts - "Add Luis and Maria Garcia as the buyers on the Birchwood Ln escrow." - "Remove the co-buyer from the Marlowe Ct escrow, keep only Dana Whitfield as buyer." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # escrows_archive > Archive an escrow to hide it from active views without deleting it. Archive an escrow to hide it from active views without deleting it. Archived escrows can be restored later with [`escrows_restore`](/tools/escrows/escrows_restore). Use this for completed or abandoned transactions you want to declutter from your dashboard. Returns the archived escrow record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow to archive. Use [`escrows_list`](/tools/escrows/escrows_list) to find the ID if unknown. | ## Example prompts - "Archive the closed Birchwood Ln escrow now that it has funded." - "Move the cancelled Quail Run escrow off my active dashboard without deleting it." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # escrows_create > Create a new escrow/transaction record. Create a new escrow/transaction record. Use when an offer is accepted and a transaction opens — to also create buyer/seller client records in one call, use [`create_escrow_with_clients`](/tools/composite/create_escrow_with_clients) instead. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. Returns the created escrow record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `property_address` | string | Yes | Full property address (street line) | | `city` | string | No | City name | | `state` | string | No | State abbreviation (e.g. "CA") | | `zip_code` | string | No | ZIP code (5 or 9 digit) | | `purchase_price` | number | No | Purchase price in dollars | | `escrow_number` | string | No | Escrow or file number | | `closing_date` | string | No | Expected closing date (YYYY-MM-DD format) | | `escrow_status` | enum | No | Status of the escrow · One of: `active`, `pending`, `closed`, `cancelled` | | `representation_type` | enum | No | Who the agent represents in this transaction · One of: `buyer`, `seller`, `dual` | | `notes` | string | No | Notes about this transaction | ## Example prompts - "Open a new escrow for 412 Birchwood Ln, Bakersfield CA 93309, 565000, closing July 15." - "Start an escrow file for the Garcias at 88 Marlowe Ct, escrow number PCH-4471." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_escrow`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # escrows_delete > Permanently delete an escrow. Permanently delete an escrow. This is IRREVERSIBLE - the escrow will be hidden from all views including archived. Only use for test data or duplicate records. For normal cleanup, use [`escrows_archive`](/tools/escrows/escrows_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow to delete. Use [`escrows_get`](/tools/escrows/escrows_get) first to verify. | ## Example prompts - "Permanently delete the duplicate Birchwood Ln escrow we entered twice last week." - "Get rid of that test escrow for good, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # escrows_get > Get full details of a specific escrow by ID — property, parties, key dates, price, commission, and status. Get full details of a specific escrow by ID — property, parties, key dates, price, commission, and status. Use after [`escrows_list`](/tools/escrows/escrows_list) when you need every field for one transaction. For multiple escrows, pass escrow_ids instead. Returns the complete escrow record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | No | UUID of a single escrow to retrieve · Format: UUID | | `escrow_ids` | array of strings | No | Array of escrow UUIDs for batch retrieval (max 100). Use either escrow_id OR escrow_ids, not both. · Max items: 100 | ## Example prompts - "Give me the full file on the Birchwood Ln escrow, every detail." - "Pull up everything on the Garcias escrow, including buyers, sellers, and commission." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_escrow`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # escrows_list > Search for escrows. Search for escrows. Returns IDs only by default for efficiency. Response includes relatedIds with BOTH clientIds and contactIds: - relatedIds.clientIds: CLIENT table IDs → use with [`clients_bulk_update`](/tools/clients/clients_bulk_update) - relatedIds.contactIds: CONTACT table IDs → use with [`contacts_get`](/tools/contacts/contacts_get) (contact_ids) ```text WORKFLOW: To mark clients as closed when escrow closes: 1. escrows_list({ status: 'closed' }) → get relatedIds.clientIds 2. clients_bulk_update({ client_ids: relatedIds.clientIds, updates: { status: 'closed' } }) ``` Direct workflow - no intermediate [`clients_list`](/tools/clients/clients_list) step needed! ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term (searches property address, escrow number) | | `status` | enum | No | Filter by escrow status (default: all) · One of: `all`, `active`, `pending`, `closed`, `cancelled`, `fell_through` | | `fields` | array of enum | No | Field groups to include. Empty/omitted = IDs only (most efficient). Options: basic (id, display_id, address, status), dates (closing_date, etc), financial (price, commission), clients (buyers/sellers), location (city, state), property (beds, baths), full (all fields). · Values: `basic`, `dates`, `financial`, `clients`, `location`, `property`, `full` | | `limit` | string or number | No | Max results. Use a number (e.g. "25") or "all". "all" and any value above 200 are capped at 200 server-side. · Max: 200 | | `offset` | number | No | Skip N records for pagination. Use pagination.nextOffset from previous response. | | `slim` | boolean | No | DEPRECATED: Use fields parameter instead. slim=true maps to fields=[] (IDs only). | ## Example prompts - "Which of my escrows are still active right now?" - "Find the escrow for 412 Birchwood Ln and show its closing date and price." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `search_escrows`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # escrows_restore > Restore a previously archived escrow back to active views. Restore a previously archived escrow back to active views. Reverses the effect of [`escrows_archive`](/tools/escrows/escrows_archive). Use this when a transaction needs to be revisited or was archived by mistake. Returns the restored escrow record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow to restore. Must be currently archived. | ## Example prompts - "Restore the Marlowe Ct escrow I archived, the buyers came back." - "Bring the archived Quail Run file back to active, that deal is reviving." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # escrows_stats > Get a summary of all escrows including counts by status and upcoming closings. Get a summary of all escrows including counts by status and upcoming closings. Use for overview questions. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How many escrows do I have open, and which close in the next two weeks?" - "Give me a quick summary of my escrows by status." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_escrow_summary`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # escrows_update > Update an existing escrow/transaction record. Update an existing escrow/transaction record. Only provided fields are updated — omitted fields remain unchanged. Common uses: move key dates, update the price, or change status as the transaction progresses. Returns the updated escrow record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow to update · Format: UUID | | `property_address` | string | No | Updated property address | | `purchase_price` | number | No | Updated purchase price | | `escrow_number` | string | No | Updated escrow number | | `closing_date` | string | No | Updated closing date (YYYY-MM-DD) | | `escrow_status` | enum | No | Updated status · One of: `active`, `pending`, `closed`, `cancelled`, `fell_through` | | `notes` | string | No | Updated notes | ## Example prompts - "Push the Birchwood Ln closing date out to August 1." - "Update the Marlowe Ct purchase price to 612500 after the appraisal renegotiation." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_escrow`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Financial calculators > Pure calculators: seller net sheet, buyer closing costs, mortgage payment, and commission split. Pure calculators: seller net sheet, buyer closing costs, mortgage payment, and commission split. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`calculate_seller_net_sheet`](/tools/financial/calculate_seller_net_sheet) | Calculator | Calculate estimated seller proceeds after all closing costs. | | [`calculate_buyer_closing_costs`](/tools/financial/calculate_buyer_closing_costs) | Calculator | Calculate estimated buyer closing costs. | | [`calculate_mortgage_payment`](/tools/financial/calculate_mortgage_payment) | Calculator | Calculate estimated monthly mortgage payment with PITI breakdown (Principal, Interest, Taxes, Insurance). | | [`calculate_commission_split`](/tools/financial/calculate_commission_split) | Calculator | Calculate estimated agent net commission after broker split, franchise fees, and transaction fees. | --- # calculate_buyer_closing_costs > Calculate estimated buyer closing costs. Calculate estimated buyer closing costs. Shows buyers their TOTAL CASH needed to close including down payment. Includes: down payment, loan origination fees, appraisal, inspection, title insurance, escrow fees, prepaid insurance, prepaid taxes, and other costs. Loan type affects costs: FHA has mortgage insurance premiums, VA has funding fee, jumbo may have higher fees. Returns itemized breakdown plus total cash needed. > [!NOTE] > These are estimates — actual lender costs may vary. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `purchase_price` | number | Yes | The purchase price of the property (required) · Min: 0 | | `down_payment_pct` | number | No | Down payment as percentage (e.g., 20 for 20%). Affects PMI requirement. · Max: 100 · Min: 0 | | `loan_type` | enum | No | Type of loan. Affects costs: FHA=MIP, VA=funding fee, jumbo=higher fees (default: conventional) · One of: `conventional`, `fha`, `va`, `jumbo` | | `interest_rate` | number | No | Annual interest rate as percentage (e.g., 6.5 for 6.5%). Used for prepaid interest calculation. · Max: 20 · Min: 0 | | `lender_fees` | number | No | Lender origination/processing fees. If not provided, estimated as 1% of loan amount. · Min: 0 | | `appraisal_fee` | number | No | Appraisal fee (default: $600) · Min: 0 | | `inspection_fee` | number | No | Home inspection fee (default: $500) · Min: 0 | | `title_insurance_pct` | number | No | Title insurance as percentage of purchase price (default: 0.5) · Max: 2 · Min: 0 | | `escrow_fee` | number | No | Escrow fee. If not provided, estimated as $2 per $1,000 of purchase price. · Min: 0 | | `prepaid_insurance` | number | No | Prepaid homeowners insurance (typically 1 year). If not provided, estimated. · Min: 0 | | `prepaid_taxes_months` | number | No | Months of property taxes to prepay into escrow (default: 3) · Max: 12 · Min: 0 | | `property_tax_rate` | number | No | Annual property tax rate as percentage (default: 1.1 for CA) · Max: 5 · Min: 0 | | `hoa_monthly` | number | No | Monthly HOA dues — affects prepaid HOA fees (default: 0) · Min: 0 | ## Example prompts - "How much cash do the Nguyens need to close at 565000 with 10 percent down FHA?" - "Estimate buyer closing costs on a 750000 purchase, conventional loan, 20 down, 6.25 rate." ## Safety **Calculator.** Pure math. Reads nothing and writes nothing in your CRM. --- # calculate_commission_split > Calculate estimated agent net commission after broker split, franchise fees, and transaction fees. Calculate estimated agent net commission after broker split, franchise fees, and transaction fees. Use when the user asks what they will actually take home from a sale. Provide gross_commission directly OR calculate from sale_price+commission_pct. Returns itemized estimate of deductions plus net-to-agent amount. > [!NOTE] > This is a simplified estimate — verify all commission calculations with your brokerage accounting. Does not account for referral fees, team splits, E&O, or brokerage caps. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `gross_commission` | number | No | Total gross commission in dollars. Provide this OR sale_price+commission_pct. · Min: 0 | | `commission_pct` | number | No | Commission percentage (used with sale_price to calculate gross_commission) · Max: 10 · Min: 0 | | `sale_price` | number | No | Sale price (used with commission_pct) · Min: 0 | | `broker_split_pct` | number | Yes | Percentage you KEEP after broker split (required, e.g., 80 for 80/20 split means you keep 80%) · Max: 100 · Min: 0 | | `referral_fee_pct` | number | No | Referral fee percentage owed to referring agent/company (default: 0) · Max: 100 · Min: 0 | | `team_split_pct` | number | No | Team lead split percentage if on a team (default: 0) · Max: 100 · Min: 0 | | `transaction_fee` | number | No | Flat transaction/admin fee charged by brokerage (default: 0) · Min: 0 | | `errors_omissions_fee` | number | No | E&O insurance fee per transaction (default: 0) · Min: 0 | | `franchise_fee_pct` | number | No | Franchise fee percentage if applicable (e.g., Keller Williams, RE/MAX) · Max: 10 · Min: 0 | ## Example prompts - "What do I net on a 565000 sale at 2.5 percent with my 80/20 split?" - "Break down my net on 14125 gross with a 70/30 split and 25 percent referral." ## Safety **Calculator.** Pure math. Reads nothing and writes nothing in your CRM. --- # calculate_mortgage_payment > Calculate estimated monthly mortgage payment with PITI breakdown (Principal, Interest, Taxes, Insurance). ```text Calculate estimated monthly mortgage payment with PITI breakdown (Principal, Interest, Taxes, Insurance). Use when a buyer asks what their monthly payment would be for a given price or loan. Includes: P&I, property taxes, homeowners insurance, HOA dues, and PMI if applicable (down payment < 20%). Can calculate from loan_amount OR from purchase_price with down_payment_pct. Returns itemized estimate of each component. NOTE: These are estimates — actual payments depend on lender terms. This is not financial advice. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `loan_amount` | number | No | Total loan amount in dollars. Provide this OR purchase_price+down_payment_pct. · Min: 0 | | `purchase_price` | number | No | Purchase price (used with down_payment_pct to calculate loan) · Min: 0 | | `down_payment_pct` | number | No | Down payment as percentage (e.g., 20 for 20%) · Max: 100 · Min: 0 | | `interest_rate` | number | Yes | Annual interest rate as percentage (required, e.g., 6.5 for 6.5%) · Max: 20 · Min: 0 | | `loan_term_years` | number | No | Loan term in years (default: 30) · Max: 40 · Min: 1 | | `property_tax_rate` | number | No | Annual property tax rate as percentage (default: 1.1 for CA) · Max: 5 · Min: 0 | | `homeowners_insurance` | number | No | Annual homeowners insurance. If not provided, estimated as 0.35% of purchase price. · Min: 0 | | `hoa_monthly` | number | No | Monthly HOA dues (default: 0) · Min: 0 | | `pmi_rate` | number | No | PMI rate as percentage of loan if down payment < 20% (default: 0.5) · Max: 3 · Min: 0 | | `include_pmi` | boolean | No | Include PMI in calculation. Auto-included if down payment < 20% (default: true if applicable) | ## Example prompts - "What is the monthly PITI on a 452000 loan at 6.5 percent over 30 years?" - "Estimate the payment at 612500, 15 percent down, 6.75 rate, 320 monthly HOA." ## Safety **Calculator.** Pure math. Reads nothing and writes nothing in your CRM. --- # calculate_seller_net_sheet > Calculate estimated seller proceeds after all closing costs. Calculate estimated seller proceeds after all closing costs. Shows sellers what they will NET from their sale. Includes: listing commission, buyer agent commission, title insurance, escrow fees, county/city transfer taxes, loan payoff, HOA fees, repairs/credits, and other costs. CALIFORNIA-SPECIFIC: Uses CA default transfer tax rates (0.11% county). For accurate city transfer tax, provide county name. Returns itemized breakdown of all costs plus estimated net proceeds. > [!NOTE] > These are estimates — actual amounts may vary based on negotiation and specific circumstances. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `sale_price` | number | Yes | The sale price of the property (required) · Min: 0 | | `mortgage_balance` | number | No | Remaining mortgage balance to pay off (default: 0) · Min: 0 | | `listing_commission_pct` | number | No | Listing agent commission percentage (e.g., 2.5 for 2.5%) · Max: 10 · Min: 0 | | `buyer_agent_commission_pct` | number | No | Buyer agent commission percentage (e.g., 2.5 for 2.5%) · Max: 10 · Min: 0 | | `title_insurance_pct` | number | No | Title insurance as percentage of sale price (default: 0.5) · Max: 2 · Min: 0 | | `escrow_fee` | number | No | Escrow fee in dollars. If not provided, estimated as $2 per $1,000 of sale price · Min: 0 | | `transfer_tax_pct` | number | No | County transfer tax percentage (default: 0.11 for CA, $1.10 per $1,000) · Max: 2 · Min: 0 | | `city_transfer_tax_pct` | number | No | City transfer tax percentage if applicable. Some CA cities have additional taxes (e.g., Oakland, Berkeley) · Max: 3 · Min: 0 | | `hoa_fees` | number | No | HOA transfer fees or outstanding dues owed (default: 0) · Min: 0 | | `repairs_credits` | number | No | Repairs or credits to buyer negotiated in contract (default: 0) · Min: 0 | | `other_costs` | number | No | Any other closing costs not listed above (default: 0) · Min: 0 | | `county` | string | No | County name for accurate transfer tax lookup (optional, uses CA default if not provided) | ## Example prompts - "Run a net sheet for the Garcias, 565000 sale, 310000 payoff, 2.5 and 2.5 commission." - "What will the sellers at 88 Marlowe Ct net at 612500 in Kern County?" ## Safety **Calculator.** Pure math. Reads nothing and writes nothing in your CRM. --- # Geographic boundaries > Search regional, neighborhood, and subdivision boundaries by place name. Search regional, neighborhood, and subdivision boundaries by place name. ## Tools (1) | Tool | Type | What it does | | --- | --- | --- | | [`geo_boundary_search`](/tools/geo-boundaries/geo_boundary_search) | Read-only | Search geographic boundaries (regional neighborhoods, neighborhoods, subdivisions) by name. | --- # geo_boundary_search > Search geographic boundaries (regional neighborhoods, neighborhoods, subdivisions) by name. ```text Search geographic boundaries (regional neighborhoods, neighborhoods, subdivisions) by name. Returns id, kind, name, centroid, and bbox for each match. Useful when the user asks about a named place ("show me The Northwest", "what neighborhood is this in"). Pass the returned id to listings_list as boundary_id to filter listings to the polygon. Results are ordered by kind priority (regional > neighborhood > subdivision) then text-search relevance — so a search for "northwest" surfaces "The Northwest" (regional) above any subdivision with that word in its name. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | Yes | Search term — matches against boundary name (case-insensitive, full-text-search). · Max length: 80 · Min length: 1 | | `limit` | integer | No | Max results (1–25). Defaults to 10. · Default: `10` · Max: 25 · Min: 1 | ## Example prompts - "Look up the boundary for The Northwest so I can pull listings inside it." - "Which neighborhoods or subdivisions match the name Seven Oaks?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Habits > Personal habit trackers — create daily, weekly, or monthly habits, log completions, and review streaks. Personal habit trackers — create daily, weekly, or monthly habits, log completions, and review streaks. ## Tools (5) | Tool | Type | What it does | | --- | --- | --- | | [`habits_list`](/tools/habits/habits_list) | Read-only | Search the authenticated user's habits (daily/weekly/monthly trackers like "Read My Bible", "Go to the Gym"). | | [`habits_get`](/tools/habits/habits_get) | Read-only | Get full details of one habit by UUID, including its per-habit checklist (step_template), tracker schema (tracker_schema), cadence, status, started_on, and best_streak. | | [`habits_create`](/tools/habits/habits_create) | Creates data | Create a new habit to track. | | [`habits_update`](/tools/habits/habits_update) | Updates data | Update an existing habit's name, cadence, status, tagline, or weekly target. | | [`habits_log`](/tools/habits/habits_log) | Updates data | Mark a habit's progress for a day (the headline action — "mark my Bible reading done today"). | --- # habits_create > Create a new habit to track. Create a new habit to track. The checklist and trackers are seeded automatically from the habit_type (bible/gym/throwing/reading/custom). Returns the created habit with its id and display_id (H-####). Cadence defaults to 'daily'. After creating, use [`habits_log`](/tools/habits/habits_log) to mark progress for a day. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `name` | string | Yes | Name of the habit (e.g. "Read My Bible", "Go to the Gym") | | `habit_type` | enum | No | Habit type — seeds the default checklist + trackers (default: custom) · One of: `bible`, `gym`, `throwing`, `reading`, `custom` | | `cadence` | enum | No | How often the habit recurs (default: daily) · One of: `daily`, `weekly`, `monthly` | | `weekly_target` | number | No | For weekly cadence — target sessions per week (e.g. 4 for a gym habit) · Max: 7 · Min: 1 | | `tagline` | string | No | A short reminder of the "why" behind the habit | | `started_on` | string | No | The date the habit started (YYYY-MM-DD). Defaults to unset. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | ## Example prompts - "Add a new record under habits." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # habits_get > Get full details of one habit by UUID, including its per-habit checklist (step_template), tracker schema (tracker_schema), cadence, status, started_on, and best_streak. Get full details of one habit by UUID, including its per-habit checklist (step_template), tracker schema (tracker_schema), cadence, status, started_on, and best_streak. If you don't have the UUID, use [`habits_list`](/tools/habits/habits_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `habit_id` | string | Yes | UUID of the habit. Use [`habits_list`](/tools/habits/habits_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up the details on one of my habits." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # habits_list > Search the authenticated user's habits (daily/weekly/monthly trackers like "Read My Bible", "Go to the Gym"). Search the authenticated user's habits (daily/weekly/monthly trackers like "Read My Bible", "Go to the Gym"). Returns pagination info (total_count, has_more, offset, limit). Each result includes: id, display_id, name, habit_type, cadence, status, started_on, best_streak, created_at. Filter by status (active/paused/archived) or habit_type. For a single habit's full detail + checklist + trackers, use [`habits_get`](/tools/habits/habits_get). To mark a habit done for a day, use [`habits_log`](/tools/habits/habits_log). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term — matches against habit name and tagline (case-insensitive partial match) | | `status` | enum | No | Filter by status (default: all) · One of: `all`, `active`, `paused`, `archived` | | `habit_type` | string | No | Filter by habit type (e.g. bible, gym, throwing, reading, custom) | | `archived` | boolean | No | Include archived habits (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Show me my habits." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # habits_log > Mark a habit's progress for a day (the headline action — "mark my Bible reading done today"). Mark a habit's progress for a day (the headline action — "mark my Bible reading done today"). Upserts the per-day log: pass done=true to mark the day complete, plus optional checklist steps and tracker metrics. Defaults log_date to today. Idempotent — calling again for the same day updates that day's log. Returns the saved log row. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `habit_id` | string | Yes | UUID of the habit to log (required). Use [`habits_list`](/tools/habits/habits_list) to find. · Format: UUID | | `log_date` | string | No | The day to log (YYYY-MM-DD). Defaults to today. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `done` | boolean | No | Whether the habit was completed for the day (default: true) | | `steps` | object (free-form) | No | Checklist completion as { stepId: boolean } — keys match the habit's step_template ids. | | `metrics` | object (free-form) | No | Tracker values as { trackerKey: value } — keys match the habit's tracker_schema keys. | | `note` | string | No | An optional note for the day | ## Example prompts - "Mark a habit's progress for a day (the headline action — "mark my Bible reading done today") — just ask in plain English." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # habits_update > Update an existing habit's name, cadence, status, tagline, or weekly target. Update an existing habit's name, cadence, status, tagline, or weekly target. Only provided fields are updated — omitted fields stay unchanged. Use status='paused' to pause a habit or status='active' to resume. Returns the updated habit. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `habit_id` | string | Yes | UUID of the habit to update (required). Use [`habits_list`](/tools/habits/habits_list) to find. · Format: UUID | | `name` | string | No | Updated habit name | | `cadence` | enum | No | Updated cadence · One of: `daily`, `weekly`, `monthly` | | `status` | enum | No | Updated status · One of: `active`, `paused`, `archived` | | `weekly_target` | number | No | Updated weekly target · Max: 7 · Min: 1 | | `tagline` | string | No | Updated tagline | ## Example prompts - "Update one of my habits." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Landing pages > Manage open-house sign-in pages and marketing landing pages, with guardrails enforced server-side. Manage open-house sign-in pages and marketing landing pages, with guardrails enforced server-side. ## Tools (7) | Tool | Type | What it does | | --- | --- | --- | | [`landing_page_create`](/tools/landing-pages/landing_page_create) | Creates data | Create a landing page for an open house sign-in or marketing purpose. | | [`landing_page_edit`](/tools/landing-pages/landing_page_edit) | Creates data + external action | Edit a landing page using natural language. | | [`landing_page_get`](/tools/landing-pages/landing_page_get) | Read-only | Get details of a landing page including status, shortcode URL, view count, submission count, and configuration. | | [`landing_page_publish`](/tools/landing-pages/landing_page_publish) | Updates data | Publish the current draft of a landing page to make it live. | | [`landing_page_revert`](/tools/landing-pages/landing_page_revert) | Irreversible | Revert a landing page draft to the last published version. | | [`landing_page_submissions`](/tools/landing-pages/landing_page_submissions) | Read-only | Get visitor sign-in submissions for a landing page. | | [`generate_open_house_flyer`](/tools/landing-pages/generate_open_house_flyer) | Read-only | Generate a professional PDF flyer for an open house. | --- # generate_open_house_flyer > Generate a professional PDF flyer for an open house. Generate a professional PDF flyer for an open house. Use when preparing marketing for an upcoming open house. Auto-populates from MLS listing data (photos, beds/baths/sqft, price, description), agent profile (headshot, contact info, DRE#), and brokerage brand (logo, colors, disclaimer). Returns a download URL for the PDF. The flyer is print-ready at 8.5" × 11" letter size. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house to generate a flyer for. | ## Example prompts - "Make a print-ready flyer for Sunday's open house at 412 Birchwood Ln." - "Generate a PDF flyer with my headshot and brokerage logo for the Garcia open house." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # landing_page_create > Create a landing page for an open house sign-in or marketing purpose. Create a landing page for an open house sign-in or marketing purpose. Optionally link it to an open house by ID to create a visitor sign-in page with a QR code. The page is created as a draft — use [`landing_page_publish`](/tools/landing-pages/landing_page_publish) to make it live. Returns: id, shortcode, publicUrl, status, title. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `title` | string | Yes | Page title (e.g. "Sign In — 123 Main St"). Required. | | `open_house_id` | string | No | UUID of the open house to link this sign-in page to. Optional — omit for standalone landing pages. | | `template_id` | enum | No | Template to use. open-house-default includes qualifying questions, open-house-minimal is name/email/phone only. Default: open-house-default. · One of: `open-house-default`, `open-house-luxury`, `open-house-minimal` | ## Example prompts - "Create an open house sign-in page for 412 Birchwood Ln with a QR code." - "Set up a minimal landing page titled Spring Buyer Seminar with just name, email, phone." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # landing_page_edit > Edit a landing page using natural language. Edit a landing page using natural language. Describe what you want to change (colors, text, layout, questions) and the AI will update the HTML/CSS. Same compliance guardrails as [`website_edit`](/tools/websites/website_edit) — Fair Housing, DRE, and XSS rules are enforced automatically. The edit is applied to the draft — use [`landing_page_publish`](/tools/landing-pages/landing_page_publish) to make it live. Examples: "Change the background to navy blue", "Add a question about school district preferences", "Make the welcome text larger". ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landing_page_id` | string | Yes | UUID of the landing page to edit. Use [`landing_page_get`](/tools/landing-pages/landing_page_get) to find. | | `prompt` | string | Yes | Natural language description of the edit. Be specific. · Max length: 2000 | ## Example prompts - "On the Birchwood sign-in page, change the background to navy and enlarge the welcome text." - "Add a question about school district preferences to my open house landing page." ## Safety **Creates data + external action.** Creates a record and triggers an action in a connected external service. --- # landing_page_get > Get details of a landing page including status, shortcode URL, view count, submission count, and configuration. Get details of a landing page including status, shortcode URL, view count, submission count, and configuration. Use this to check the current state before editing or publishing. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landing_page_id` | string | Yes | UUID of the landing page. | ## Example prompts - "How many views and sign-ups does the 412 Birchwood Ln landing page have so far?" - "Check whether my Maple Grove open house page is still in draft or live." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # landing_page_publish > Publish the current draft of a landing page to make it live. Publish the current draft of a landing page to make it live. The page will be accessible at its public URL. Only works if there is draft content to publish. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landing_page_id` | string | Yes | UUID of the landing page to publish. | ## Example prompts - "Publish the draft sign-in page for Saturday's open house at 412 Birchwood Ln." - "Make my Spring Buyer Seminar landing page live now." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # landing_page_revert > Revert a landing page draft to the last published version. Revert a landing page draft to the last published version. Discards all unpublished changes. Only works if there is a published version to revert to. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landing_page_id` | string | Yes | UUID of the landing page to revert. | ## Example prompts - "Throw away my draft edits and roll the Birchwood page back to the published version." - "Revert the seminar page to its last published version, but double-check with me before discarding anything." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # landing_page_submissions > Get visitor sign-in submissions for a landing page. Get visitor sign-in submissions for a landing page. Returns contact info (name, email, phone), qualifying answers, and whether each visitor completed step 1 only or both steps. Use this to review who visited an open house. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landing_page_id` | string | Yes | UUID of the landing page. | | `limit` | number | No | Max results (default: 25). · Max: 100 · Min: 1 | | `page` | number | No | Page number (default: 1). · Min: 1 | ## Example prompts - "Who signed in at the 412 Birchwood Ln open house yesterday?" - "Pull the visitor submissions from my Maple Grove page, including their qualifying answers." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Leads > Capture, qualify, update, and convert leads into clients. Capture, qualify, update, and convert leads into clients. ## Tools (9) | Tool | Type | What it does | | --- | --- | --- | | [`leads_create`](/tools/leads/leads_create) | Creates data | Create a new lead in the CRM. | | [`leads_list`](/tools/leads/leads_list) | Read-only | Search for leads by status, source, or text. | | [`leads_update`](/tools/leads/leads_update) | Updates data | Update an existing lead's information or status. | | [`leads_get`](/tools/leads/leads_get) | Read-only | Get full details of a specific lead by ID — contact info, source, status, and timeline fields. | | [`leads_convert`](/tools/leads/leads_convert) | Irreversible | Convert a lead to a client when they become an active buyer or seller. | | [`leads_archive`](/tools/leads/leads_archive) | Updates data | Archive a lead to hide them from active views without deleting their record. | | [`leads_restore`](/tools/leads/leads_restore) | Updates data | Restore a previously archived lead back to active views. | | [`leads_delete`](/tools/leads/leads_delete) | Irreversible | Permanently delete a lead. | | [`leads_stats`](/tools/leads/leads_stats) | Read-only | Get aggregate statistics for all leads: counts by status (new/contacted/qualified/converted/lost), conversion rate, and counts by source. | --- # leads_archive > Archive a lead to hide them from active views without deleting their record. Archive a lead to hide them from active views without deleting their record. Archived leads can be restored later with [`leads_restore`](/tools/leads/leads_restore). Use this for leads that have gone cold or been disqualified but you want to keep their history. Returns the archived lead record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | Yes | UUID of the lead to archive. Use [`leads_list`](/tools/leads/leads_list) to find the ID if unknown. | ## Example prompts - "Archive the Henderson lead, they went cold after three follow-ups." - "Hide Maria's old lead from my active pipeline but keep the history." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # leads_convert > Convert a lead to a client when they become an active buyer or seller. Convert a lead to a client when they become an active buyer or seller. Use at the moment a representation agreement is signed or the lead commits to working with you. Returns the new client record linked to the original lead. > [!WARNING] > This is IRREVERSIBLE — once a lead is marked converted, the source/timeline data freezes for analytics. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | Yes | UUID of the lead to convert · Format: UUID | | `client_type` | enum | No | Type of client they are becoming · One of: `buyer`, `seller`, `both` | ## Example prompts - "The Garcias signed a buyer agreement, so convert their lead to a buyer client." - "Convert Tom Nguyen to a seller client, but confirm with me first since it is irreversible." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. ## Previous name This tool was previously published as `convert_lead_to_client`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # leads_create > Create a new lead in the CRM. Create a new lead in the CRM. Use when a new prospective buyer or seller comes in from any source. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. Returns the created lead record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `first_name` | string | Yes | First name of the lead | | `last_name` | string | Yes | Last name of the lead | | `email` | string | No | Email address | | `phone` | string | No | Phone number | | `source` | string | No | Lead source (e.g., referral, zillow, open_house, website, cold_call) | | `notes` | string | No | Notes about this lead | | `lead_type` | enum | No | Type of lead · One of: `buyer`, `seller`, `both`, `investor`, `renter` | ## Example prompts - "Add a new buyer lead: Maria Garcia, 661-555-0142, met her at the Birchwood open house." - "Create a seller lead for Tom Nguyen from Zillow, email tom.nguyen@gmail.com." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_lead`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # leads_delete > Permanently delete a lead. Permanently delete a lead. This is IRREVERSIBLE - the lead will be hidden from all views including archived. Only use for test data or duplicate records. For normal cleanup, use [`leads_archive`](/tools/leads/leads_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | Yes | UUID of the lead to delete. Use [`leads_get`](/tools/leads/leads_get) first to verify. | ## Example prompts - "Permanently delete that duplicate lead record for Maria Garcia." - "Delete the test lead I created earlier, but double-check with me before removing it permanently." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # leads_get > Get full details of a specific lead by ID — contact info, source, status, and timeline fields. Get full details of a specific lead by ID — contact info, source, status, and timeline fields. Use after [`leads_list`](/tools/leads/leads_list) when you need every field for one lead. For multiple leads, pass lead_ids instead. Returns the complete lead record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | No | UUID of a single lead to retrieve · Format: UUID | | `lead_ids` | array of strings | No | Array of lead UUIDs for batch retrieval (max 100). Use either lead_id OR lead_ids, not both. · Max items: 100 | ## Example prompts - "Pull up everything we have on the Garcia lead." - "Show me full details for these three leads I flagged this morning." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_lead`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # leads_list > Search for leads by status, source, or text. Search for leads by status, source, or text. Use to find lead IDs before updating or converting, or to answer questions about the lead pipeline. Returns IDs only by default for efficiency; use the fields parameter to include additional data. Response includes relatedIds.contactIds for chainable operations. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term (searches name, email, phone) | | `status` | enum | No | Filter by lead status · One of: `new`, `contacted`, `qualified`, `unqualified`, `converted`, `lost` | | `source` | string | No | Filter by lead source | | `fields` | array of enum | No | Field groups to include. Empty/omitted = IDs only. Options: basic (id, type, status), contact (name, email, phone), source (source, score), timeline (dates), full (all fields). · Values: `basic`, `contact`, `source`, `timeline`, `full` | | `limit` | string | No | Max results. Use a number (e.g. "25") or "all". "all" and any value above 200 are capped at 200 server-side. | ## Example prompts - "Show me all my qualified leads that came from open houses." - "Find leads named Garcia and include their contact info and lead score." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `search_leads`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # leads_restore > Restore a previously archived lead back to active views. Restore a previously archived lead back to active views. Reverses the effect of [`leads_archive`](/tools/leads/leads_archive). Use this when a cold lead re-engages or was archived by mistake. Returns the restored lead record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | Yes | UUID of the lead to restore. Must be currently archived. | ## Example prompts - "The Hendersons just called back, restore their archived lead." - "Unarchive Tom Nguyen's lead, I archived it by mistake last week." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # leads_stats > Get aggregate statistics for all leads: counts by status (new/contacted/qualified/converted/lost), conversion rate, and counts by source. Get aggregate statistics for all leads: counts by status (new/contacted/qualified/converted/lost), conversion rate, and counts by source. Use this for dashboard metrics and pipeline analysis. Does NOT return individual lead records - use [`leads_list`](/tools/leads/leads_list) for that. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "What's my lead conversion rate and how many new leads this month?" - "Break down my pipeline by lead status and source for me." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # leads_update > Update an existing lead's information or status. Update an existing lead's information or status. Only provided fields are updated — omitted fields remain unchanged. Common uses: advance status as the lead warms up, correct contact details, or record the source. Returns the updated lead record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lead_id` | string | Yes | UUID of the lead to update · Format: UUID | | `first_name` | string | No | Updated first name | | `last_name` | string | No | Updated last name | | `email` | string | No | Updated email | | `phone` | string | No | Updated phone | | `status` | enum | No | Updated status · One of: `new`, `contacted`, `qualified`, `unqualified`, `converted`, `lost` | | `notes` | string | No | Notes to add | ## Example prompts - "Mark Maria Garcia's lead as contacted and note that she wants a callback Friday." - "Update Tom Nguyen's phone to 661-555-0198 and set his status to qualified." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_lead`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Leases > Lease record management for the property-management vertical. Lease record management for the property-management vertical. ## Tools (5) | Tool | Type | What it does | | --- | --- | --- | | [`leases_list`](/tools/leases/leases_list) | Read-only | Search the user's leases by status, lease_type, party, or free-text query. | | [`leases_get`](/tools/leases/leases_get) | Read-only | Get full details of one lease by UUID. | | [`leases_create`](/tools/leases/leases_create) | Creates data | Create a new lease agreement. | | [`leases_expiring`](/tools/leases/leases_expiring) | Read-only | List leases expiring within the next N days (default 60, max 365). | | [`leases_stats`](/tools/leases/leases_stats) | Read-only | Get aggregate stats for the user's leases: total count, breakdown by_status, active_monthly_rent_roll (sum of monthly_rent for active leases — useful for "how much recurring rental income am I managing"), and expiring_in_60_days count. | --- # leases_create > Create a new lease agreement. Create a new lease agreement. Use when a new lease is being drafted or activated outside the normal application→lease flow. For the common case of approving an application and executing the lease, use [`rental_applications_convert_to_lease`](/tools/rental-applications/rental_applications_convert_to_lease) instead — that atomically approves + creates + links. Required: lease_start, lease_end (ISO dates), monthly_rent. Defaults: status='draft', lease_type='residential'. Returns the created lease record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | No | UUID of the listing being leased · Format: UUID | | `landlord_contact_id` | string | No | UUID of the landlord contact · Format: UUID | | `renter_client_id` | string | No | UUID of the renter client (typically client_type=tenant) · Format: UUID | | `rental_application_id` | string | No | UUID of the approved application (if converted from one) · Format: UUID | | `previous_lease_id` | string | No | UUID of the prior lease (for renewals — links the chain) · Format: UUID | | `lease_start` | string | Yes | Lease start date (ISO format, YYYY-MM-DD) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `lease_end` | string | Yes | Lease end date (ISO format, YYYY-MM-DD). Must be after lease_start. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `monthly_rent` | number | Yes | Monthly rent amount · Min: 0 | | `security_deposit` | number | No | Security deposit amount · Min: 0 | | `status` | enum | No | Initial status (default: draft) · One of: `draft`, `active`, `expiring`, `renewed`, `terminated`, `expired` | | `lease_type` | enum | No | Lease type (default: residential) · One of: `residential`, `commercial`, `short_term` | | `commission_amount` | number | No | Agent commission for this lease · Min: 0 | | `notes` | string | No | Free-text notes · Max length: 10000 | ## Example prompts - "Draft a lease for the Patels at 88 Calloway Ave, 2400 monthly, July 1 to June 30." - "Create a 12-month residential lease starting August 1 at 1950 monthly with a 2000 deposit." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # leases_expiring > List leases expiring within the next N days (default 60, max 365). List leases expiring within the next N days (default 60, max 365). Returns active + expiring leases ordered by lease_end ascending — closest expiration first. Use this to surface renewal opportunities, proactive outreach, or renewal-vs-sale pitches. This is the primary tool for the "who needs attention this quarter" question. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days` | number | No | Lookahead window in days (default: 60) · Max: 365 · Min: 0 | ## Example prompts - "Which of my leases expire in the next 90 days?" - "Show me upcoming lease expirations so I can pitch renewals this quarter." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # leases_get > Get full details of one lease by UUID. Get full details of one lease by UUID. Returns all lease fields including renter/landlord references, commission, renewal chain (previous_lease_id), and the rental_application_id that led to the lease. Use [`leases_list`](/tools/leases/leases_list) first if you don't have the UUID. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lease_id` | string | Yes | UUID of the lease · Format: UUID | ## Example prompts - "Pull the full terms of the Patel lease, including deposit and renewal chain." - "What's the commission on the lease at 88 Calloway Ave?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # leases_list > Search the user's leases by status, lease_type, party, or free-text query. Search the user's leases by status, lease_type, party, or free-text query. Returns id, lease_start, lease_end, monthly_rent, status, lease_type, renter_client_id, landlord_contact_id, listing_id. Use status='expiring' or the dedicated [`leases_expiring`](/tools/leases/leases_expiring) tool to find leases ending soon — the dedicated view is more useful when looking for renewal opportunities. Pagination: total_count + has_more + offset + limit. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Free-text search (reserved — not currently indexed; use specific filters instead) | | `status` | enum | No | Filter by lease status. active=currently occupied, expiring=within renewal window, renewed=has successor lease, terminated=ended early, expired=passed lease_end with no renewal · One of: `draft`, `active`, `expiring`, `renewed`, `terminated`, `expired` | | `lease_type` | enum | No | Filter by lease type · One of: `residential`, `commercial`, `short_term` | | `renter_client_id` | string | No | Filter to leases for a specific renter (client). Use [`clients_list`](/tools/clients/clients_list) with client_type=tenant to find tenants first. · Format: UUID | | `landlord_contact_id` | string | No | Filter to leases for a specific landlord (contact). Use [`contacts_list`](/tools/contacts/contacts_list) with leasing_role=landlord to find landlords first. · Format: UUID | | `listing_id` | string | No | Filter to leases for a specific listing · Format: UUID | | `limit` | number | No | Maximum results (default: 25) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "List all my active residential leases with their monthly rent." - "Show every lease tied to the 88 Calloway Ave listing." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # leases_stats > Get aggregate stats for the user's leases: total count, breakdown by_status, active_monthly_rent_roll (sum of monthly_rent for active leases — useful for "how much recurring rental income am I managing"), and expiring_in_60_days count. Get aggregate stats for the user's leases: total count, breakdown by_status, active_monthly_rent_roll (sum of monthly_rent for active leases — useful for "how much recurring rental income am I managing"), and expiring_in_60_days count. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How much monthly rent roll am I managing across active leases?" - "Give me lease counts by status and how many expire within 60 days." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Listings > Your property inventory — create, search, update, and manage listings. Your property inventory — create, search, update, and manage listings. ## Tools (9) | Tool | Type | What it does | | --- | --- | --- | | [`listings_list`](/tools/listings/listings_list) | Read-only | Search for property listings by address, MLS number, status, or price range. | | [`listings_get`](/tools/listings/listings_get) | Read-only | Get full details of one or more listings by UUID. | | [`listings_create`](/tools/listings/listings_create) | Creates data | Create a new property listing. | | [`listings_update`](/tools/listings/listings_update) | Updates data | Update an existing listing's information or status. | | [`listings_stats`](/tools/listings/listings_stats) | Read-only | Get aggregate statistics for all listings: counts by status (active/pending/sold/expired), total active listings value, and average days on market. | | [`listings_expiring`](/tools/listings/listings_expiring) | Read-only | Get listings approaching or past their expiration date. | | [`listings_archive`](/tools/listings/listings_archive) | Updates data | Archive a listing to hide it from active views without deleting it. | | [`listings_restore`](/tools/listings/listings_restore) | Updates data | Restore a previously archived listing back to active views. | | [`listings_delete`](/tools/listings/listings_delete) | Irreversible | Permanently delete a listing. | --- # listings_archive > Archive a listing to hide it from active views without deleting it. Archive a listing to hide it from active views without deleting it. Archived listings can be restored later with [`listings_restore`](/tools/listings/listings_restore). Use this for expired or sold listings you want to declutter from your dashboard. Returns the archived listing record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | Yes | UUID of the listing to archive. Use [`listings_list`](/tools/listings/listings_list) to find the ID if unknown. · Format: UUID | ## Example prompts - "Archive the sold listing at 901 Marsh Creek Dr to declutter my dashboard." - "Hide that expired Birchwood listing from my active views, keep the record." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # listings_create > Create a new property listing. Create a new property listing. Call immediately with whatever data the user provides. Do NOT ask for missing fields — the system shows an editable draft card where the user can fill in remaining details. WORKFLOW: 1) Use [`lookup_address`](/tools/utility/lookup_address) first to get verified address data with coordinates 2) Create the listing with property details 3) Optionally schedule open houses with [`create_open_house`](/tools/deadline/create_open_house). Returns the created listing with ID. Required fields: property_address and list_price. May trigger notifications if integrations are configured. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `property_address` | string | Yes | Full property address (required). Use [`lookup_address`](/tools/utility/lookup_address) first for verified data. | | `display_address` | string | No | Display address if different (e.g., "123 Main St, Unit 4B"). | | `city` | string | No | City. | | `state` | string | No | State abbreviation (e.g., CA). | | `zip_code` | string | No | ZIP code. | | `county` | string | No | County name. | | `latitude` | number | No | Latitude coordinate (from [`lookup_address`](/tools/utility/lookup_address)). | | `longitude` | number | No | Longitude coordinate (from [`lookup_address`](/tools/utility/lookup_address)). | | `list_price` | number | Yes | Listing price in dollars (required). · Min: 0 | | `mls_number` | string | No | MLS number (RESO ListingId). | | `bedrooms` | number | No | Number of bedrooms. · Min: 0 | | `bathrooms` | number | No | Number of bathrooms (can be decimal for half baths). · Min: 0 | | `square_feet` | number | No | Square footage (RESO LivingArea). · Min: 0 | | `lot_size` | number | No | Lot size in square feet. | | `year_built` | number | No | Year built. | | `garage_spaces` | number | No | Number of garage spaces. | | `stories` | number | No | Number of stories. | | `property_type` | enum | No | Type of property (RESO PropertyType). · One of: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `commercial` | | `property_sub_type` | string | No | RESO PropertySubType (e.g., Attached, Detached). | | `listing_date` | string | No | Date listing goes active on MLS. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `expiration_date` | string | No | Listing agreement expiration date — important for tracking. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `listing_status` | enum | No | Initial listing status. · One of: `active`, `pending`, `coming_soon` | | `description` | string | No | Property description (also sets public_remarks). | | `public_remarks` | string | No | RESO PublicRemarks (MLS description). | | `showing_instructions` | string | No | RESO ShowingInstructions. | | `virtual_tour_link` | string | No | Virtual tour URL (branded). | | `list_agent_mls_id` | string | No | RESO ListAgentMlsId. | | `list_agent_full_name` | string | No | RESO ListAgentFullName. | | `list_office_name` | string | No | RESO ListOfficeName. | ## Example prompts - "Create a listing for 412 Birchwood Ln at 489,000, 4 bed 3 bath, 2,150 square feet." - "List the Garcias' condo at 901 Marsh Creek Dr for 315,000 as coming soon." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. ## Previous name This tool was previously published as `create_listing`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # listings_delete > Permanently delete a listing. Permanently delete a listing. This is IRREVERSIBLE — the listing will be hidden from all views, including archived. Only use for test data or duplicate records. For normal cleanup, use [`listings_archive`](/tools/listings/listings_archive) instead which is reversible. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | Yes | UUID of the listing to delete. Use [`listings_get`](/tools/listings/listings_get) first to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate listing record for 412 Birchwood Ln." - "Remove that test listing for good, but double-check with me before deleting it permanently." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # listings_expiring > Get listings approaching or past their expiration date. Get listings approaching or past their expiration date. PROSPECTING TOOL: Expiring listings are opportunities to acquire new business — contact sellers whose listings are about to expire. Returns listings sorted by expiration date. Use days_until_expiration to set the lookback window. include_expired=true also shows recently expired listings (within expired_within_days). Default shows active listings expiring in next 30 days plus expired within 14 days. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_until_expiration` | number | No | Show listings expiring within X days (default: 30). · Min: 1 | | `include_expired` | boolean | No | Include recently expired listings (default: true). | | `expired_within_days` | number | No | If including expired, how many days back to look (default: 14). · Min: 1 | ## Example prompts - "Which listings in my pipeline expire within the next 30 days?" - "Show recently expired listings so I can call those sellers about relisting." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # listings_get > Get full details of one or more listings by UUID. Get full details of one or more listings by UUID. Supports BOTH single and batch modes: pass listing_id for one listing, or listing_ids array for efficient multi-fetch (up to 100 IDs). Single mode returns all listing fields including full description and MLS data. Batch mode returns array of listings with count and not_found list. If you don't have the UUID, use [`listings_list`](/tools/listings/listings_list) first. For matching listings to buyers, use [`match_buyers_to_listings`](/tools/reporting/match_buyers_to_listings). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | No | UUID of a single listing. Use [`listings_list`](/tools/listings/listings_list) to find IDs. · Format: UUID | | `listing_ids` | array of strings | No | Array of listing UUIDs for batch retrieval (max 100). More efficient than multiple single calls. · Max items: 100 | ## Example prompts - "Pull the full MLS details for the 412 Birchwood Ln listing." - "Fetch complete details for these five listing IDs in one batch." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_listing`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # listings_list > Search for property listings by address, MLS number, status, or price range. Search for property listings by address, MLS number, status, or price range. Returns pagination info (total_count, has_more, offset, limit) for batch operations. Results include: id, property_address, city, state, zip_code, list_price, listing_status, property_type, bedrooms, bathrooms, square_feet, created_at. Filter by status to find active listings vs sold/expired. Use min_price/max_price to match buyer budgets. For a specific listing by ID, use [`listings_get`](/tools/listings/listings_get) instead. For aggregate stats, use [`listings_stats`](/tools/listings/listings_stats). Default limit is 20 when not provided; if the default is used and more results exist, the response includes truncated: true and total: N so you can rerun with a higher limit. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term — matches against property_address, city, and mls_number (case-insensitive partial match). | | `status` | enum | No | Filter by listing status. active=on market, pending=under contract, sold=closed, expired=listing ended, coming_soon=pre-market (default: all). · One of: `all`, `active`, `pending`, `sold`, `expired`, `cancelled`, `coming_soon` | | `min_price` | number | No | Minimum listing price in dollars. Use with max_price for buyer matching. · Min: 0 | | `max_price` | number | No | Maximum listing price in dollars. Use with min_price for buyer matching. · Min: 0 | | `property_type` | enum | No | Filter by property type. · One of: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `commercial` | | `limit` | number | No | Maximum results (default: 20, max: 500). When omitted and more results exist, response includes truncated: true + total: N. · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0). · Min: 0 | ## Example prompts - "Find active single family listings between 400k and 550k for the Garcias." - "Search my listings for anything on Birchwood Ln in Bakersfield." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `search_listings`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # listings_restore > Restore a previously archived listing back to active views. Restore a previously archived listing back to active views. Reverses the effect of [`listings_archive`](/tools/listings/listings_archive). Use this when a listing needs to be reactivated or was archived by mistake. Returns the restored listing record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | Yes | UUID of the listing to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived Marsh Creek listing, the sellers want to relist." - "Unarchive 412 Birchwood Ln, I archived it by accident." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # listings_stats > Get aggregate statistics for all listings: counts by status (active/pending/sold/expired), total active listings value, and average days on market. Get aggregate statistics for all listings: counts by status (active/pending/sold/expired), total active listings value, and average days on market. Use this for dashboard metrics and pipeline overview. Does NOT return individual listing records — use [`listings_list`](/tools/listings/listings_list) for that. Returns statusCounts array and activeListingsValue total. For specific listings, use [`listings_list`](/tools/listings/listings_list) with filters. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "What's my total active listings value and average days on market?" - "Give me a count of my listings by status for the dashboard." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. ## Previous name This tool was previously published as `get_listings_summary`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # listings_update > Update an existing listing's information or status. Update an existing listing's information or status. Only provided fields are updated — omitted fields remain unchanged. Common uses: price reduction (update list_price), status change (active→pending→sold), address correction, extend expiration date. Status transitions: active → pending (offer accepted) → sold (closed) or expired/cancelled (listing ended). Returns the updated listing record. If the listing_id is not found, the error suggests using [`listings_list`](/tools/listings/listings_list). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `listing_id` | string | Yes | UUID of the listing to update (required). Use [`listings_list`](/tools/listings/listings_list) to find the ID if unknown. · Format: UUID | | `property_address` | string | No | Updated street address. | | `display_address` | string | No | Updated display address (e.g., with unit number). | | `city` | string | No | Updated city name. | | `state` | string | No | Updated state abbreviation. | | `zip_code` | string | No | Updated ZIP code. | | `county` | string | No | Updated county name. | | `latitude` | number | No | Updated latitude coordinate. | | `longitude` | number | No | Updated longitude coordinate. | | `list_price` | number | No | Updated listing price (price reduction or increase). · Min: 0 | | `mls_number` | string | No | Updated MLS listing number. | | `bedrooms` | number | No | Updated bedroom count. · Min: 0 | | `bathrooms` | number | No | Updated bathroom count (can be decimal for half baths). · Min: 0 | | `square_feet` | number | No | Updated square footage. · Min: 0 | | `property_type` | enum | No | Updated property type (rare — usually fixed at creation). · One of: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `commercial` | | `close_price` | number | No | Close/sale price (RESO ClosePrice). | | `close_date` | string | No | Close date (ISO 8601, RESO CloseDate). | | `listing_date` | string | No | Updated date listing went active on MLS. (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `expiration_date` | string | No | Updated expiration date (for extensions). (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `listing_status` | enum | No | Updated status. active→pending→sold is the typical flow. · One of: `active`, `pending`, `sold`, `expired`, `cancelled` | | `description` | string | No | Updated description. | | `public_remarks` | string | No | RESO PublicRemarks. | | `private_remarks` | string | No | RESO PrivateRemarks (agent-only notes). | | `buyer_agent_mls_id` | string | No | RESO BuyerAgentMlsId. | | `buyer_agent_full_name` | string | No | RESO BuyerAgentFullName. | ## Example prompts - "Drop the price on 412 Birchwood Ln to 475,000." - "Mark the Marsh Creek condo pending, we accepted an offer this morning." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. ## Previous name This tool was previously published as `update_listing`. Call it by its current name — legacy names are kept here for reference and old links redirect, but they are not callable. --- # Open houses > Schedule and manage open-house events and their visitor records. Schedule and manage open-house events and their visitor records. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`open_houses_list`](/tools/open-houses/open_houses_list) | Read-only | Search for open house events by address, status, or date range. | | [`open_houses_get`](/tools/open-houses/open_houses_get) | Read-only | Get full details of an open house by UUID, including all visitors. | | [`open_houses_create`](/tools/open-houses/open_houses_create) | Creates data | Schedule a new open house event. | | [`open_houses_update`](/tools/open-houses/open_houses_update) | Updates data | Update an existing open house's information or status. | | [`open_houses_stats`](/tools/open-houses/open_houses_stats) | Read-only | Get aggregate statistics for open houses: total count, upcoming count, completed count, total visitors, average visitors per event, and next 5 upcoming events. | | [`open_houses_archive`](/tools/open-houses/open_houses_archive) | Updates data | Archive an open house to hide it from active views without deleting it. | | [`open_houses_restore`](/tools/open-houses/open_houses_restore) | Updates data | Restore a previously archived open house back to active views. | | [`open_houses_delete`](/tools/open-houses/open_houses_delete) | Irreversible | Permanently delete an open house. | --- # open_houses_archive > Archive an open house to hide it from active views without deleting it. Archive an open house to hide it from active views without deleting it. Archived open houses can be restored later with [`open_houses_restore`](/tools/open-houses/open_houses_restore). Use this for past events you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house to archive. Use [`open_houses_list`](/tools/open-houses/open_houses_list) to find. · Format: UUID | ## Example prompts - "Archive last month's completed open houses at Birchwood to clean up my dashboard." - "Hide the cancelled Marsh Creek open house from my active views." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # open_houses_create > Schedule a new open house event. Schedule a new open house event. WORKFLOW: 1) Optionally use [`listings_list`](/tools/listings/listings_list) to find the listing 2) Create the open house with address, date, and time 3) Use [`log_open_house_visitor`](/tools/deadline/log_open_house_visitor) to log visitors during the event. Returns the created open house with ID. Required fields: address, date, start_time, end_time. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | Property address for the open house (required) | | `listing_id` | string | No | UUID of the associated listing (optional). Use [`listings_list`](/tools/listings/listings_list) to find. · Format: UUID | | `list_price` | number | No | Property list price in dollars · Min: 0 | | `date` | string | Yes | Date of the open house (required) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `start_time` | string | Yes | Start time in HH:MM format (required), e.g. "13:00" | | `end_time` | string | Yes | End time in HH:MM format (required), e.g. "16:00" | | `status` | enum | No | Initial status (default: scheduled) · One of: `scheduled`, `in_progress`, `completed`, `cancelled` | | `notes` | string | No | Notes or special instructions for the open house | ## Example prompts - "Schedule an open house at 412 Birchwood Ln this Saturday from 1 to 4." - "Set up a Sunday open house for the Marsh Creek condo, noon to three." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # open_houses_delete > Permanently delete an open house. Permanently delete an open house. This is IRREVERSIBLE - the open house will be hidden from all views, including archived. The open house must be archived first. For normal cleanup, use [`open_houses_archive`](/tools/open-houses/open_houses_archive) instead which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house to delete. Must be archived first. Use [`open_houses_get`](/tools/open-houses/open_houses_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete that duplicate open house entry for 412 Birchwood Ln." - "Delete the archived test open house, but double-check with me before removing it permanently." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # open_houses_get > Get full details of an open house by UUID, including all visitors. Get full details of an open house by UUID, including all visitors. Returns address, date/time, status, notes, visitor_count, and full visitor list with contact info and interest level. If you don't have the UUID, use [`open_houses_list`](/tools/open-houses/open_houses_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house. Use [`open_houses_list`](/tools/open-houses/open_houses_list) to find IDs. · Format: UUID | ## Example prompts - "Show me the sign-in list from Sunday's open house at 412 Birchwood Ln." - "Pull the details and visitors for that Birchwood open house, including interest levels." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # open_houses_list > Search for open house events by address, status, or date range. Search for open house events by address, status, or date range. Returns pagination info (total_count, has_more, offset, limit). Results include: id, address, list_price, date, start_time, end_time, status, visitor_count, notes, created_at. Filter by status (scheduled/in_progress/completed/cancelled) or date range. For a specific open house by ID, use [`open_houses_get`](/tools/open-houses/open_houses_get) instead. For aggregate stats, use [`open_houses_stats`](/tools/open-houses/open_houses_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against address (case-insensitive partial match) | | `status` | enum | No | Filter by status. scheduled=upcoming, in_progress=happening now, completed=past, cancelled=cancelled (default: all) · One of: `all`, `scheduled`, `in_progress`, `completed`, `cancelled` | | `date_from` | string | No | Show open houses on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show open houses on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived open houses (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "What open houses do I have scheduled for this weekend?" - "List completed open houses at 412 Birchwood Ln with their visitor counts." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # open_houses_restore > Restore a previously archived open house back to active views. Restore a previously archived open house back to active views. Reverses the effect of [`open_houses_archive`](/tools/open-houses/open_houses_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived Birchwood open house, I still need its visitor list." - "Unarchive Sunday's open house event, it was archived by mistake." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # open_houses_stats > Get aggregate statistics for open houses: total count, upcoming count, completed count, total visitors, average visitors per event, and next 5 upcoming events. Get aggregate statistics for open houses: total count, upcoming count, completed count, total visitors, average visitors per event, and next 5 upcoming events. Use this for dashboard metrics. Does NOT return individual records - use [`open_houses_list`](/tools/open-houses/open_houses_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How many visitors did my open houses average over the last three months?" - "Give me open house metrics for the year plus my next five upcoming events." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # open_houses_update > Update an existing open house's information or status. Update an existing open house's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: reschedule (update date/times), status change (scheduled→in_progress→completed), add notes. Returns the updated open house record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `open_house_id` | string | Yes | UUID of the open house to update (required). Use [`open_houses_list`](/tools/open-houses/open_houses_list) to find. · Format: UUID | | `address` | string | No | Updated property address | | `listing_id` | string | No | Updated listing ID · Format: UUID | | `list_price` | number | No | Updated list price · Min: 0 | | `date` | string | No | Updated date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `start_time` | string | No | Updated start time in HH:MM format | | `end_time` | string | No | Updated end time in HH:MM format | | `status` | enum | No | Updated status · One of: `scheduled`, `in_progress`, `completed`, `cancelled` | | `notes` | string | No | Updated notes | ## Example prompts - "Move Saturday's Birchwood open house to 2 until 5 instead." - "Mark the Marsh Creek open house completed and note we had heavy foot traffic." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Partners (vendor pipeline) > Manage vendor-partner relationships — create, update, stats, and lifecycle operations. Manage vendor-partner relationships — create, update, stats, and lifecycle operations. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`partners_list`](/tools/partners/partners_list) | Read-only | Search for vendor partners by name, status, vendor type, or tier. | | [`partners_get`](/tools/partners/partners_get) | Read-only | Get full details of a vendor partner by UUID, including contact info, vendor type, tier, partner_since date, notes, tags, and relationship history. | | [`partners_create`](/tools/partners/partners_create) | Creates data | Create a new vendor partner to track an established vendor relationship. | | [`partners_update`](/tools/partners/partners_update) | Updates data | Update an existing vendor partner's information or status. | | [`partners_stats`](/tools/partners/partners_stats) | Read-only | Get aggregate statistics for vendor partners: total count, counts by status, counts by tier, counts by vendor type, active vs churned ratio. | | [`partners_archive`](/tools/partners/partners_archive) | Updates data | Archive a vendor partner to hide it from active views without deleting it. | | [`partners_restore`](/tools/partners/partners_restore) | Updates data | Restore a previously archived vendor partner back to active views. | | [`partners_delete`](/tools/partners/partners_delete) | Irreversible | Permanently delete a vendor partner. | --- # partners_archive > Archive a vendor partner to hide it from active views without deleting it. Archive a vendor partner to hide it from active views without deleting it. Archived partners can be restored later with [`partners_restore`](/tools/partners/partners_restore). Use this for churned or paused partners you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `partner_id` | string | Yes | UUID of the partner to archive. Use [`partners_list`](/tools/partners/partners_list) to find. · Format: UUID | ## Example prompts - "Archive the churned contractor partner record for Reyes Construction." - "Hide Lena Ortiz's paused partnership from my active roster without losing the history." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # partners_create > Create a new vendor partner to track an established vendor relationship. Create a new vendor partner to track an established vendor relationship. WORKFLOW: 1) Optionally use [`contacts_list`](/tools/contacts/contacts_list) to find the contact 2) Create the partner with vendor_type and relationship details. Returns the created partner with ID and display_id. Tier defaults to 'standard' if not specified. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of an existing contact for this partner. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | Yes | Type of vendor (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `title` | string | No | Title or name for this partner record | | `tier` | enum | No | Partner tier level (default: standard) · One of: `standard`, `preferred`, `vip` | | `partner_since` | string | No | Date the partnership began (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `notes` | string | No | Additional notes about the partner relationship | | `tags` | array of strings | No | Tags for categorization (e.g. ["reliable", "fast-turnaround", "bilingual"]) | ## Example prompts - "Add Marcus Webb at Summit Lending as a new preferred lender partner since March." - "Create a photographer partner record for Lena Ortiz, tag her fast-turnaround and bilingual." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # partners_delete > Permanently delete a vendor partner. Permanently delete a vendor partner. This is IRREVERSIBLE - the partner will be hidden from all views including archived. The partner must be archived first. For normal cleanup, use [`partners_archive`](/tools/partners/partners_archive) instead, which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `partner_id` | string | Yes | UUID of the partner to delete. Must be archived first. Use [`partners_get`](/tools/partners/partners_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate partner record for Dana Whitfield." - "Delete that archived test partner for good, but double-check with me before removing it." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # partners_get > Get full details of a vendor partner by UUID, including contact info, vendor type, tier, partner_since date, notes, tags, and relationship history. Get full details of a vendor partner by UUID, including contact info, vendor type, tier, partner_since date, notes, tags, and relationship history. Returns all fields. If you don't have the UUID, use [`partners_list`](/tools/partners/partners_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `partner_id` | string | Yes | UUID of the partner. Use [`partners_list`](/tools/partners/partners_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up the full relationship history for my title rep Dana Whitfield." - "What tier is Marcus Webb in and how long has he been a partner?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # partners_list > Search for vendor partners by name, status, vendor type, or tier. Search for vendor partners by name, status, vendor type, or tier. Returns pagination info (total_count, has_more, offset, limit). Results include: id, display_id, title, vendor_type, status, tier, partner_since, care_status, contact info, created_at. Filter by status (onboarding/active/preferred/paused/churned), tier (standard/preferred/vip), vendor_type, or care_status. For a specific partner by ID, use [`partners_get`](/tools/partners/partners_get) instead. For aggregate stats, use [`partners_stats`](/tools/partners/partners_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against title, notes (case-insensitive partial match) | | `status` | enum | No | Filter by status. onboarding=setting up, active=regular partner, preferred=top partner, paused=temporarily inactive, churned=relationship ended (default: all) · One of: `all`, `onboarding`, `active`, `preferred`, `paused`, `churned` | | `tier` | enum | No | Filter by partner tier (default: all) · One of: `all`, `standard`, `preferred`, `vip` | | `vendor_type` | string | No | Filter by vendor type (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `care_status` | enum | No | Filter by care status (default: all) · One of: `all`, `no_status`, `cared`, `needs_care`, `didnt_care` | | `date_from` | string | No | Show partners created on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show partners created on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived partners (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Show me all my preferred tier lender partners." - "List active inspector partners flagged as needing care this month." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # partners_restore > Restore a previously archived vendor partner back to active views. Restore a previously archived vendor partner back to active views. Reverses the effect of [`partners_archive`](/tools/partners/partners_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `partner_id` | string | Yes | UUID of the partner to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived partner record for Reyes Construction, they're back on board." - "Unarchive Marcus Webb's partner record, I shelved it by mistake." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # partners_stats > Get aggregate statistics for vendor partners: total count, counts by status, counts by tier, counts by vendor type, active vs churned ratio. Get aggregate statistics for vendor partners: total count, counts by status, counts by tier, counts by vendor type, active vs churned ratio. Use this for dashboard metrics. Does NOT return individual records - use [`partners_list`](/tools/partners/partners_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How many active versus churned partners do I have this year?" - "Break down my partner roster by vendor type and tier." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # partners_update > Update an existing vendor partner's information or status. ```text Update an existing vendor partner's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: status change (onboarding->active->preferred), tier upgrade, update notes. Returns the updated partner record. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `partner_id` | string | Yes | UUID of the partner to update (required). Use [`partners_list`](/tools/partners/partners_list) to find. · Format: UUID | | `contact_id` | string | No | Updated contact ID. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | No | Updated vendor type | | `title` | string | No | Updated title | | `status` | enum | No | Updated status · One of: `onboarding`, `active`, `preferred`, `paused`, `churned` | | `tier` | enum | No | Updated partner tier · One of: `standard`, `preferred`, `vip` | | `partner_since` | string | No | Updated partnership start date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `notes` | string | No | Updated notes | | `tags` | array of strings | No | Updated tags array (replaces existing tags) | | `care_status` | enum | No | Care status for tracking. Setting this also updates the audit columns. · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `care_status_note` | string | No | Note explaining the care status change. | | `record_data` | object (free-form) | No | Pipeline-specific vertical data as a JSON object. Replaces (does not merge with) the existing record_data. | ## Example prompts - "Bump Dana Whitfield to VIP tier, she closed three escrows with us this quarter." - "Mark Marcus Webb as needs care and note we haven't touched base since April." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Pipelines > Config-driven pipeline engine: overview, record management, stage moves, and batch stage operations. Config-driven pipeline engine: overview, record management, stage moves, and batch stage operations. ## Tools (15) | Tool | Type | What it does | | --- | --- | --- | | [`pipeline_overview`](/tools/pipelines/pipeline_overview) | Read-only | Get pipeline counts grouped by industry vertical for the current user. | | [`pipeline_types_list`](/tools/pipelines/pipeline_types_list) | Read-only | List available pipeline types for the current user based on their role. | | [`pipeline_stages_list`](/tools/pipelines/pipeline_stages_list) | Read-only | Get the ordered stages (funnel) for a specific pipeline type. | | [`pipeline_records_create`](/tools/pipelines/pipeline_records_create) | Creates data | Create a new pipeline record. | | [`pipeline_records_list`](/tools/pipelines/pipeline_records_list) | Read-only | List pipeline records with filtering. | | [`pipeline_records_get`](/tools/pipelines/pipeline_records_get) | Read-only | Get a single pipeline record by ID with full details including contact info, current stage, pipeline type, and stage-specific fields. | | [`pipeline_records_update`](/tools/pipelines/pipeline_records_update) | Updates data | Update a pipeline record's fields — title, source, estimated_ltv, priority, notes, tags, record_data, and more. | | [`pipeline_records_move_stage`](/tools/pipelines/pipeline_records_move_stage) | Updates data | Move a pipeline record to a new stage. | | [`pipeline_records_delete`](/tools/pipelines/pipeline_records_delete) | Irreversible | Delete a pipeline record. | | [`pipeline_records_archive`](/tools/pipelines/pipeline_records_archive) | Updates data | Archive a pipeline record to hide it from default views without deleting it. | | [`pipeline_records_restore`](/tools/pipelines/pipeline_records_restore) | Updates data | Restore an archived pipeline record back to active status. | | [`pipeline_records_stats`](/tools/pipelines/pipeline_records_stats) | Read-only | Get pipeline statistics: funnel counts per stage, total LTV, total revenue, conversion rate, records needing follow-up, and records expiring soon. | | [`pipeline_record_activities`](/tools/pipelines/pipeline_record_activities) | Read-only | Get the activity history for a pipeline record — stage changes, notes, calls, meetings, and other logged activities in reverse chronological order. | | [`pipeline_record_log_activity`](/tools/pipelines/pipeline_record_log_activity) | Logs activity | Log an activity on a pipeline record. | | [`pipeline_records_batch_stage`](/tools/pipelines/pipeline_records_batch_stage) | Bulk changes | Move multiple pipeline records to the same stage. | --- # pipeline_overview > Get pipeline counts grouped by industry vertical for the current user. Get pipeline counts grouped by industry vertical for the current user. Returns lead, appointment, client, and stage-4 counts per vertical. Useful for understanding pipeline volume across different business verticals. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "How is my pipeline volume looking across each industry vertical right now?" - "Give me a quick count of leads, appointments, and clients per vertical." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_record_activities > Get the activity history for a pipeline record — stage changes, notes, calls, meetings, and other logged activities in reverse chronological order. Get the activity history for a pipeline record — stage changes, notes, calls, meetings, and other logged activities in reverse chronological order. Use to review what has happened on a record before the next touch. Returns the activity list. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `record_id` | string | Yes | Pipeline record UUID · Format: UUID | | `limit` | integer | No | Max activities to return (default: 20) · Max: 100 · Min: 1 | ## Example prompts - "Show me the full activity history on the Smith Brokerage record." - "What stage changes and calls have been logged for Keystone Realty lately?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_record_log_activity > Log an activity on a pipeline record. Log an activity on a pipeline record. Use for notes, calls, emails, meetings, demos, or tasks. Creates a timestamped entry in the record's activity history. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `record_id` | string | Yes | Pipeline record UUID · Format: UUID | | `activity_type` | enum | Yes | Type of activity · One of: `note`, `call`, `email`, `meeting`, `demo`, `task`, `other` | | `subject` | string | No | Activity subject/title | | `body` | string | No | Activity details/notes | | `details` | object | No | Structured activity data | ## Example prompts - "Log a call on the Smith Brokerage record, we discussed pricing for 15 seats." - "Add a note to the Keystone Realty record about their demo scheduled Friday." ## Safety **Logs activity.** Adds an activity log entry. Your existing records are not changed. --- # pipeline_records_archive > Archive a pipeline record to hide it from default views without deleting it. Archive a pipeline record to hide it from default views without deleting it. Use for stale or inactive records you want out of the way — restore later with [`pipeline_records_restore`](/tools/pipelines/pipeline_records_restore). Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | ## Example prompts - "Archive the Hillcrest Lending record, that deal has gone completely cold." - "Hide the Smith Brokerage record from my active pipeline without deleting it." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # pipeline_records_batch_stage > Move multiple pipeline records to the same stage. Move multiple pipeline records to the same stage. All records must belong to the same pipeline type. Logs individual stage transitions for each record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `ids` | array of strings | Yes | Array of pipeline record UUIDs · Max items: 100 | | `stage_key` | string | Yes | Target stage key | | `notes` | string | No | Notes about the batch stage change | ## Example prompts - "Move all five of my trial accounts to the paid stage at once." - "Batch move these stalled demo records back to lead, but confirm the list with me first." ## Safety **Bulk changes.** Changes many records in one call. Review the scope carefully before confirming — bulk changes are treated as destructive because of their blast radius. --- # pipeline_records_create > Create a new pipeline record. Create a new pipeline record. Provide contact_id for an existing contact, OR first_name + last_name (+ optional email/phone) to auto-create a contact. The record starts at the default (first) stage. Returns the created record with ID for subsequent operations. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `pipeline_type_id` | string | Yes | Pipeline type UUID (use [`pipeline_types_list`](/tools/pipelines/pipeline_types_list) to find) · Format: UUID | | `contact_id` | string | No | Existing contact UUID (optional if providing name/email) · Format: UUID | | `first_name` | string | No | First name for auto-creating a contact | | `last_name` | string | No | Last name for auto-creating a contact | | `email` | string | No | Email address · Format: Email address | | `phone` | string | No | Phone number | | `company` | string | No | Company name | | `title` | string | No | Record title (e.g., "Smith Brokerage — 15 seats") | | `source` | string | No | Lead source (referral, website, ad, cold_call, etc.) | | `estimated_ltv` | number | No | Estimated lifetime value in dollars | | `expected_close_date` | string | No | Expected conversion date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `priority` | enum | No | Priority level · One of: `low`, `medium`, `high`, `urgent` | | `notes` | string | No | Notes about this record | | `record_data` | object | No | Stage-specific fields as key-value pairs | ## Example prompts - "Add Smith Brokerage as a new pipeline record with an estimated 18000 dollar lifetime value." - "Create a pipeline record for Dana Whitfield at Keystone Realty, high priority, source networking event." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # pipeline_records_delete > Delete a pipeline record. Delete a pipeline record. Use for records created by mistake or test data — for records you may need again, prefer [`pipeline_records_archive`](/tools/pipelines/pipeline_records_archive). The record is marked as deleted and hidden from all views, but not permanently removed. Returns confirmation of the deletion. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | ## Example prompts - "Delete the duplicate pipeline record I accidentally created for Smith Brokerage." - "Remove that test record from my pipeline, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # pipeline_records_get > Get a single pipeline record by ID with full details including contact info, current stage, pipeline type, and stage-specific fields. Get a single pipeline record by ID with full details including contact info, current stage, pipeline type, and stage-specific fields. Use after [`pipeline_records_list`](/tools/pipelines/pipeline_records_list) when you need everything about one record. Returns the complete record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | ## Example prompts - "Pull up the full pipeline record for Smith Brokerage with its stage details." - "What are the details on the Keystone Realty record, including stage-specific fields?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_records_list > List pipeline records with filtering. List pipeline records with filtering. Use to browse a pipeline or find record IDs before get/update/move operations. Filter by stage, care status, priority, source, or search by name/email/title. Returns records with contact info and stage details. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `pipeline_type_id` | string | No | Filter by pipeline type · Format: UUID | | `stage_key` | string | No | Filter by stage key (e.g., "lead", "demo", "trial") | | `care_status` | enum | No | Filter by care status · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `priority` | enum | No | Filter by priority level · One of: `low`, `medium`, `high`, `urgent` | | `source` | string | No | Filter by source | | `search` | string | No | Search by contact name, email, or record title | | `is_archived` | boolean | No | Include archived records (default: false) | | `page` | integer | No | Page number (default: 1) · Min: 1 | | `limit` | integer | No | Results per page (default: 25) · Max: 100 · Min: 1 | | `sort_by` | enum | No | Sort field · One of: `created_at`, `updated_at`, `estimated_ltv`, `expected_close_date`, `priority` | | `sort_order` | enum | No | Sort direction · One of: `asc`, `desc` | ## Example prompts - "List everyone sitting in the demo stage of my pipeline right now." - "Show urgent pipeline records that still need care, sorted by expected close date." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_records_move_stage > Move a pipeline record to a new stage. Move a pipeline record to a new stage. Logs the stage transition in the activity history. Use [`pipeline_stages_list`](/tools/pipelines/pipeline_stages_list) to see available stage keys. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | | `stage_key` | string | Yes | Target stage key (e.g., "demo", "trial", "paid") | | `notes` | string | No | Notes about why this record moved stages | ## Example prompts - "Move Smith Brokerage from demo to trial, they started their evaluation today." - "Advance the Keystone Realty record to the paid stage and note they signed yesterday." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # pipeline_records_restore > Restore an archived pipeline record back to active status. Restore an archived pipeline record back to active status. Use when a previously archived record becomes relevant again. Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | ## Example prompts - "Restore the archived Hillcrest Lending record, they just reached back out." - "Bring the Smith Brokerage record back into my active pipeline view." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # pipeline_records_stats > Get pipeline statistics: funnel counts per stage, total LTV, total revenue, conversion rate, records needing follow-up, and records expiring soon. Get pipeline statistics: funnel counts per stage, total LTV, total revenue, conversion rate, records needing follow-up, and records expiring soon. Use to answer how a pipeline is doing or to decide where to focus follow-up. Returns the aggregate numbers only — use [`pipeline_records_list`](/tools/pipelines/pipeline_records_list) to see the underlying records. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `pipeline_type_id` | string | No | Filter stats by pipeline type · Format: UUID | ## Example prompts - "What is my pipeline conversion rate and total LTV right now?" - "How many pipeline records need follow-up or are expiring soon?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_records_update > Update a pipeline record's fields — title, source, estimated_ltv, priority, notes, tags, record_data, and more. Update a pipeline record's fields — title, source, estimated_ltv, priority, notes, tags, record_data, and more. Only provided fields are updated. To change stage, use [`pipeline_records_move_stage`](/tools/pipelines/pipeline_records_move_stage) instead. Returns the updated record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `id` | string | Yes | Pipeline record UUID · Format: UUID | | `title` | string | No | Record title | | `source` | string | No | Lead/record source | | `estimated_ltv` | number | No | Estimated lifetime value | | `current_revenue` | number | No | Current revenue amount | | `expected_close_date` | string | No | Expected conversion date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `expiration_date` | string | No | Expiration date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `priority` | enum | No | Priority level · One of: `low`, `medium`, `high`, `urgent` | | `notes` | string | No | Free-text notes | | `tags` | array of strings | No | Tags for categorization (replaces existing tags) | | `record_data` | object | No | Stage-specific fields (merged with existing) | | `probability` | integer | No | Close probability percentage (0-100) · Max: 100 · Min: 0 | | `care_status` | enum | No | Care status · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `care_status_note` | string | No | Note for care status change | ## Example prompts - "Bump the Smith Brokerage record to urgent priority and set close probability to 80." - "Update the Keystone Realty record with a 24000 dollar estimated LTV and add my call notes." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # pipeline_stages_list > Get the ordered stages (funnel) for a specific pipeline type. Get the ordered stages (funnel) for a specific pipeline type. Each stage has a key, label, color, probability, and whether it is a default/final/lost stage. Use [`pipeline_types_list`](/tools/pipelines/pipeline_types_list) first to get the pipeline_type_id. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `pipeline_type_id` | string | Yes | Pipeline type UUID from [`pipeline_types_list`](/tools/pipelines/pipeline_types_list) · Format: UUID | ## Example prompts - "Show me the stages in my lender pipeline from first contact to close." - "What funnel steps does my vendor growth pipeline go through, in order?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pipeline_types_list > List available pipeline types for the current user based on their role. List available pipeline types for the current user based on their role. System admins see the SaaS Growth Pipeline. Vendors and lenders see their industry-specific pipeline. Returns pipeline type IDs needed for other pipeline operations. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Which pipelines do I have access to with my lender account?" - "List my available pipeline types so I can pick the right one." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # P&L statements > Analyze an uploaded profit-and-loss statement and predict expense categories. Analyze an uploaded profit-and-loss statement and predict expense categories. ## Tools (2) | Tool | Type | What it does | | --- | --- | --- | | [`analyze_pl_statement`](/tools/pl-statements/analyze_pl_statement) | Read-only | Analyze an uploaded Profit & Loss statement the caller owns. | | [`categorize_expense`](/tools/pl-statements/categorize_expense) | Read-only | Predict the expense category for a single ad-hoc expense input (description + optional vendor + optional amount). | --- # analyze_pl_statement > Analyze an uploaded Profit & Loss statement the caller owns. Analyze an uploaded Profit & Loss statement the caller owns. Returns the parsed summary (period, totals, line-item count, top categories), prior-period totals when available, and the AI-generated recommendations attached to the statement. The caller MUST own the statement — requests for statements belonging to another account return an error. Use when the user asks "what does my P&L tell me", "analyze my Q1 numbers", or similar. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `pl_statement_id` | string | Yes | UUID of the P&L statement to analyze. Must be owned by the caller. | ## Example prompts - "Analyze my Q1 profit and loss statement and tell me what stands out." - "What does my latest P&L say about my top expense categories?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # categorize_expense > Predict the expense category for a single ad-hoc expense input (description + optional vendor + optional amount). Predict the expense category for a single ad-hoc expense input (description + optional vendor + optional amount). Returns category, subcategory, confidence (0–1), and the source of the prediction (per-user vendor map, platform-wide vendor map, keyword rules, or none). NOT a write — does not record the expense. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `description` | string | No | Free-text description of the expense (required if vendor not given). | | `vendor` | string | No | Vendor or payee name. Strongest signal for the categorizer. | | `amount_cents` | integer | No | Amount in cents. Optional — used as a tie-breaker but not required. · Min: 0 | ## Example prompts - "How should I categorize a 450 dollar charge from Bay Photo Marketing?" - "What expense category does an 89 dollar lockbox purchase from SentriLock fall under?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # PM consultations > Manage property-management consultation requests from prospective landlord clients. Manage property-management consultation requests from prospective landlord clients. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`pm_consultations_list`](/tools/pm-consultations/pm_consultations_list) | Read-only | Search property-management (PM) consultations — the funnel for turning landlord contacts into management clients. | | [`pm_consultations_get`](/tools/pm-consultations/pm_consultations_get) | Read-only | Get full details of one consultation by UUID. | | [`pm_consultations_create`](/tools/pm-consultations/pm_consultations_create) | Creates data | Start a PM consultation — a new conversation with a landlord about potentially handing over property management. | | [`pm_consultations_convert_to_tenancy`](/tools/pm-consultations/pm_consultations_convert_to_tenancy) | Creates data | Atomically mark a consultation as signed and create a linked tenancy. | --- # pm_consultations_convert_to_tenancy > Atomically mark a consultation as signed and create a linked tenancy. Atomically mark a consultation as signed and create a linked tenancy. Requires lease_id (the lease that now gets managed) and typically owner_client_id (the landlord as a client). The new tenancy automatically inherits the consultation's brokerage/team assignment. This is the canonical "we've signed this landlord, start managing the lease" action. Cannot convert a declined consultation. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `consultation_id` | string | Yes | UUID of the consultation to convert · Format: UUID | | `lease_id` | string | Yes | UUID of the lease that will now be PM-managed (required) · Format: UUID | | `owner_client_id` | string | No | UUID of the landlord as a client (typically client_type=landlord) · Format: UUID | | `management_fee_pct` | number | No | Actual fee on signing (default: proposed_management_fee_pct from consultation, or 10.00) · Max: 100 · Min: 0 | | `rent_collection_day` | number | No | Day of month for rent collection (default: 1) · Max: 31 · Min: 1 | | `notes` | string | No | Initial notes on the new tenancy · Max length: 10000 | ## Example prompts - "Marcus Bell signed the management agreement, convert his consultation and start managing the lease." - "Mark the Nair consultation as signed and set rent collection for the first." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # pm_consultations_create > Start a PM consultation — a new conversation with a landlord about potentially handing over property management. Start a PM consultation — a new conversation with a landlord about potentially handing over property management. Defaults stage='initial_inquiry'. Add property_listing_ids if the landlord has specific properties under discussion. Set estimated_monthly_revenue + close_probability to surface weighted pipeline value in stats. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `landlord_contact_id` | string | Yes | UUID of the landlord contact (required) · Format: UUID | | `property_listing_ids` | array of strings | No | UUIDs of listings the landlord is considering for PM · Format: UUID | | `stage` | enum | No | Initial stage (default: initial_inquiry) · One of: `initial_inquiry`, `property_walkthrough`, `proposal_sent`, `contract_negotiation`, `signed`, `declined`, `stalled` | | `proposed_management_fee_pct` | number | No | Proposed monthly management fee percentage (0-100) · Max: 100 · Min: 0 | | `proposed_leasing_fee_months` | number | No | Months of rent charged as leasing fee when placing a new tenant · Min: 0 | | `estimated_monthly_revenue` | number | No | Estimated recurring monthly revenue if signed · Min: 0 | | `close_probability` | number | No | Probability of closing (0-100, used for weighted pipeline stats) · Max: 100 · Min: 0 | | `stage_notes` | string | No | Notes about the current stage (discussion points, next steps, objections) · Max length: 10000 | ## Example prompts - "Start a management consultation with landlord Priya Nair for her duplex on Birchwood Ln." - "Open a PM consultation with the Garcias, 8 percent fee, around 600 monthly revenue." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # pm_consultations_get > Get full details of one consultation by UUID. Get full details of one consultation by UUID. Includes sensitive proposal terms (proposed_management_fee_pct, proposed_leasing_fee_months, estimated_monthly_revenue, close_probability) — caller must sanitize before any public AI summary. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `consultation_id` | string | Yes | UUID of the consultation · Format: UUID | ## Example prompts - "Pull up my consultation with landlord Marcus Bell, including the proposed management fee." - "What terms did I propose in the Ramos consultation, and the close probability?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # pm_consultations_list > Search property-management (PM) consultations — the funnel for turning landlord contacts into management clients. Search property-management (PM) consultations — the funnel for turning landlord contacts into management clients. Returns id, landlord_contact_id, stage, proposed_management_fee_pct, estimated_monthly_revenue, close_probability, next_meeting_at. Filter by stage to find consultations at a specific funnel step (e.g. stage='proposal_sent' for "who am I waiting on?"). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Free-text search (reserved) | | `stage` | enum | No | Filter by funnel stage · One of: `initial_inquiry`, `property_walkthrough`, `proposal_sent`, `contract_negotiation`, `signed`, `declined`, `stalled` | | `landlord_contact_id` | string | No | Filter to consultations with a specific landlord · Format: UUID | | `limit` | number | No | Maximum results (default: 25, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Which landlords have I sent management proposals to and not heard back from?" - "List my property management consultations currently in contract negotiation." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Prospects (vendor pipeline) > Top-of-funnel vendor prospecting — create, update, stats, and lifecycle operations. Top-of-funnel vendor prospecting — create, update, stats, and lifecycle operations. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`prospects_list`](/tools/prospects/prospects_list) | Read-only | Search for vendor prospects by name, status, vendor type, or interest level. | | [`prospects_get`](/tools/prospects/prospects_get) | Read-only | Get full details of a vendor prospect by UUID, including contact info, vendor type, interest level, priority, estimated lifetime value, notes, tags, and status history. | | [`prospects_create`](/tools/prospects/prospects_create) | Creates data | Create a new vendor prospect to track a potential partner. | | [`prospects_update`](/tools/prospects/prospects_update) | Updates data | Update an existing vendor prospect's information or status. | | [`prospects_stats`](/tools/prospects/prospects_stats) | Read-only | Get aggregate statistics for vendor prospects: total count, counts by status, counts by vendor type, counts by interest level, average estimated LTV. | | [`prospects_archive`](/tools/prospects/prospects_archive) | Updates data | Archive a vendor prospect to hide it from active views without deleting it. | | [`prospects_restore`](/tools/prospects/prospects_restore) | Updates data | Restore a previously archived vendor prospect back to active views. | | [`prospects_delete`](/tools/prospects/prospects_delete) | Irreversible | Permanently delete a vendor prospect. | --- # prospects_archive > Archive a vendor prospect to hide it from active views without deleting it. Archive a vendor prospect to hide it from active views without deleting it. Archived prospects can be restored later with [`prospects_restore`](/tools/prospects/prospects_restore). Use this for disqualified or lost prospects you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prospect_id` | string | Yes | UUID of the prospect to archive. Use [`prospects_list`](/tools/prospects/prospects_list) to find. · Format: UUID | ## Example prompts - "Archive the disqualified appraiser prospect so it stops cluttering my dashboard." - "Hide the Coastal Title prospect from my active list, the deal went cold." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # prospects_create > Create a new vendor prospect to track a potential partner. Create a new vendor prospect to track a potential partner. WORKFLOW: 1) Optionally use [`contacts_list`](/tools/contacts/contacts_list) to find the contact 2) Create the prospect with vendor_type and details. Returns the created prospect with ID and display_id. Interest level defaults to 'warm' and priority to 'medium' if not specified. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `contact_id` | string | No | UUID of an existing contact for this prospect. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | Yes | Type of vendor (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `title` | string | No | Title or name for this prospect record | | `source` | string | No | How you discovered this prospect (e.g. "referral", "networking event", "online search") | | `interest_level` | enum | No | Interest level of the prospect (default: warm) · One of: `cold`, `warm`, `hot` | | `priority` | enum | No | Priority for outreach (default: medium) · One of: `low`, `medium`, `high`, `urgent` | | `estimated_ltv` | number | No | Estimated lifetime value of this vendor relationship in dollars · Min: 0 | | `notes` | string | No | Additional notes about the prospect | | `tags` | array of strings | No | Tags for categorization (e.g. ["local", "high-volume", "spanish-speaking"]) | ## Example prompts - "Add Tom Avery from Summit Lending as a hot prospect worth about 15000." - "Create a new photographer prospect for Lena Brooks, met her at a networking event." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # prospects_delete > Permanently delete a vendor prospect. Permanently delete a vendor prospect. This is IRREVERSIBLE - the prospect will be hidden from all views including archived. The prospect must be archived first. For normal cleanup, use [`prospects_archive`](/tools/prospects/prospects_archive) instead, which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prospect_id` | string | Yes | UUID of the prospect to delete. Must be archived first. Use [`prospects_get`](/tools/prospects/prospects_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate prospect record I created for Summit Lending." - "Clear out that archived test prospect, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # prospects_get > Get full details of a vendor prospect by UUID, including contact info, vendor type, interest level, priority, estimated lifetime value, notes, tags, and status history. Get full details of a vendor prospect by UUID, including contact info, vendor type, interest level, priority, estimated lifetime value, notes, tags, and status history. Returns all fields. If you don't have the UUID, use [`prospects_list`](/tools/prospects/prospects_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prospect_id` | string | Yes | UUID of the prospect. Use [`prospects_list`](/tools/prospects/prospects_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up everything on the prospect Rosa Delgado, including notes and status history." - "What is the estimated lifetime value on my Coastal Title prospect?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # prospects_list > Search for vendor prospects by name, status, vendor type, or interest level. Search for vendor prospects by name, status, vendor type, or interest level. Returns pagination info (total_count, has_more, offset, limit). Results include: id, display_id, title, vendor_type, status, interest_level, priority, estimated_ltv, care_status, contact info, created_at. Filter by status (identified/researching/outreach/responded/nurturing/converted/disqualified/lost), interest_level (cold/warm/hot), priority (low/medium/high/urgent), or vendor_type. For a specific prospect by ID, use [`prospects_get`](/tools/prospects/prospects_get) instead. For aggregate stats, use [`prospects_stats`](/tools/prospects/prospects_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against title, notes (case-insensitive partial match) | | `status` | enum | No | Filter by status. identified=new prospect, researching=gathering info, outreach=initial contact, responded=they replied, nurturing=building relationship, converted=became partner, disqualified=not a fit, lost=went cold (default: all) · One of: `all`, `identified`, `researching`, `outreach`, `responded`, `nurturing`, `converted`, `disqualified`, `lost` | | `vendor_type` | string | No | Filter by vendor type (e.g. lender, inspector, escrow_officer, title_rep, appraiser, contractor, photographer) | | `interest_level` | enum | No | Filter by interest level (default: all) · One of: `all`, `cold`, `warm`, `hot` | | `priority` | enum | No | Filter by priority (default: all) · One of: `all`, `low`, `medium`, `high`, `urgent` | | `care_status` | enum | No | Filter by care status (default: all) · One of: `all`, `no_status`, `cared`, `needs_care`, `didnt_care` | | `date_from` | string | No | Show prospects created on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show prospects created on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived prospects (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Show me all my hot lender prospects still in the outreach stage." - "List inspector prospects added since March that have responded to me." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # prospects_restore > Restore a previously archived vendor prospect back to active views. Restore a previously archived vendor prospect back to active views. Reverses the effect of [`prospects_archive`](/tools/prospects/prospects_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prospect_id` | string | Yes | UUID of the prospect to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived Tom Avery prospect, he just emailed me back." - "Bring my Coastal Title prospect back into the active pipeline." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # prospects_stats > Get aggregate statistics for vendor prospects: total count, counts by status, counts by vendor type, counts by interest level, average estimated LTV. Get aggregate statistics for vendor prospects: total count, counts by status, counts by vendor type, counts by interest level, average estimated LTV. Use this for dashboard metrics. Does NOT return individual records - use [`prospects_list`](/tools/prospects/prospects_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How many vendor prospects did I add this month, broken down by status?" - "What is my average prospect lifetime value over the past year?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # prospects_update > Update an existing vendor prospect's information or status. ```text Update an existing vendor prospect's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: status change (identified->researching->outreach->responded->nurturing->converted), update interest level, add notes. Returns the updated prospect record. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prospect_id` | string | Yes | UUID of the prospect to update (required). Use [`prospects_list`](/tools/prospects/prospects_list) to find. · Format: UUID | | `contact_id` | string | No | Updated contact ID. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `vendor_type` | string | No | Updated vendor type | | `title` | string | No | Updated title | | `source` | string | No | Updated source | | `status` | enum | No | Updated status · One of: `identified`, `researching`, `outreach`, `responded`, `nurturing`, `converted`, `disqualified`, `lost` | | `interest_level` | enum | No | Updated interest level · One of: `cold`, `warm`, `hot` | | `priority` | enum | No | Updated priority · One of: `low`, `medium`, `high`, `urgent` | | `estimated_ltv` | number | No | Updated estimated lifetime value · Min: 0 | | `notes` | string | No | Updated notes | | `tags` | array of strings | No | Updated tags array (replaces existing tags) | | `care_status` | enum | No | Care status for tracking. Setting this also updates the audit columns. · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | | `care_status_note` | string | No | Note explaining the care status change. | | `record_data` | object (free-form) | No | Pipeline-specific vertical data as a JSON object. Replaces (does not merge with) the existing record_data. | ## Example prompts - "Mark the Rosa Delgado prospect as responded and bump her to high priority." - "Move my Summit Lending prospect to nurturing and note our coffee meeting Tuesday." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Referrals > Agent-to-agent referrals — create, update, stats, and lifecycle operations. Agent-to-agent referrals — create, update, stats, and lifecycle operations. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`referrals_list`](/tools/referrals/referrals_list) | Read-only | Search for referrals by name, status, type, or date range. | | [`referrals_get`](/tools/referrals/referrals_get) | Read-only | Get full details of a referral by UUID, including referred person info, referring agent, receiving agent, financial details, and linked escrow. | | [`referrals_create`](/tools/referrals/referrals_create) | Creates data | Create a new referral to track an agent-to-agent referral. | | [`referrals_update`](/tools/referrals/referrals_update) | Updates data | Update an existing referral's information or status. | | [`referrals_stats`](/tools/referrals/referrals_stats) | Read-only | Get aggregate statistics for referrals: total count, counts by status, total fees earned, total fees pending. | | [`referrals_archive`](/tools/referrals/referrals_archive) | Updates data | Archive a referral to hide it from active views without deleting it. | | [`referrals_restore`](/tools/referrals/referrals_restore) | Updates data | Restore a previously archived referral back to active views. | | [`referrals_delete`](/tools/referrals/referrals_delete) | Irreversible | Permanently delete a referral. | --- # referrals_archive > Archive a referral to hide it from active views without deleting it. Archive a referral to hide it from active views without deleting it. Archived referrals can be restored later with [`referrals_restore`](/tools/referrals/referrals_restore). Use this for old or inactive referrals you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referral_id` | string | Yes | UUID of the referral to archive. Use [`referrals_list`](/tools/referrals/referrals_list) to find. · Format: UUID | ## Example prompts - "Archive the expired referral for Mark Tibbets to clean up my dashboard." - "Hide that old declined referral from my active list without deleting it." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # referrals_create > Create a new referral to track an agent-to-agent referral. Create a new referral to track an agent-to-agent referral. WORKFLOW: 1) Optionally use [`leads_list`](/tools/leads/leads_list) or [`contacts_list`](/tools/contacts/contacts_list) to find the referred person 2) Create the referral with referred person info and receiving agent details. Returns the created referral with ID and display_id. If referral_fee_percentage is not specified, it defaults to your brokerage's configured rate, or 25%. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referred_name` | string | No | Name of the referred person | | `referred_email` | string | No | Email of the referred person | | `referred_phone` | string | No | Phone number of the referred person | | `referred_lead_id` | string | No | UUID of an existing lead being referred. Use [`leads_list`](/tools/leads/leads_list) to find. · Format: UUID | | `referred_contact_id` | string | No | UUID of an existing contact being referred. Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `receiving_agent_name` | string | No | Name of the agent receiving the referral | | `receiving_agent_email` | string | No | Email of the receiving agent | | `receiving_agent_id` | string | No | UUID of the receiving agent if they are on the platform · Format: UUID | | `referring_agent_name` | string | No | Name of the referring agent (auto-filled from current user if omitted) | | `referring_agent_email` | string | No | Email of the referring agent | | `referral_type` | enum | No | Type of referral (default: buyer) · One of: `buyer`, `seller`, `both` | | `referral_fee_percentage` | number | No | Referral fee percentage (default: your brokerage's configured rate, or 25%) · Max: 100 · Min: 0 | | `referral_source` | string | No | How the referral originated (e.g. "sphere of influence", "past client", "open house") | | `notes` | string | No | Additional notes about the referral | ## Example prompts - "Refer the Garcias to Dana Cole in Phoenix with a 25 percent fee." - "Create a buyer referral sending Mark Tibbets to agent Joy Lin at Coastal Realty." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # referrals_delete > Permanently delete a referral. Permanently delete a referral. This is IRREVERSIBLE - the referral will be hidden from all views including archived. The referral must be archived first. For normal cleanup, use [`referrals_archive`](/tools/referrals/referrals_archive) instead, which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referral_id` | string | Yes | UUID of the referral to delete. Must be archived first. Use [`referrals_get`](/tools/referrals/referrals_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete the duplicate referral I logged twice for the Garcias." - "Remove that archived test referral for good, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # referrals_get > Get full details of a referral by UUID, including referred person info, referring agent, receiving agent, financial details, and linked escrow. Get full details of a referral by UUID, including referred person info, referring agent, receiving agent, financial details, and linked escrow. Returns all fields including status history timestamps. If you don't have the UUID, use [`referrals_list`](/tools/referrals/referrals_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referral_id` | string | Yes | UUID of the referral. Use [`referrals_list`](/tools/referrals/referrals_list) to find IDs. · Format: UUID | ## Example prompts - "Pull up the full referral details for the Garcias, including the fee terms." - "What is the status history on my referral to agent Dana Cole?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # referrals_list > Search for referrals by name, status, type, or date range. Search for referrals by name, status, type, or date range. Returns pagination info (total_count, has_more, offset, limit). Results include: id, display_id, referred_name, referred_email, referring_agent_name, receiving_agent_name, referral_type, status, referral_fee_percentage, created_at. Filter by status (submitted/accepted/active/closed/paid/declined/expired), type (buyer/seller/both), or date range. For a specific referral by ID, use [`referrals_get`](/tools/referrals/referrals_get) instead. For aggregate stats, use [`referrals_stats`](/tools/referrals/referrals_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against referred_name, referred_email, referring_agent_name, receiving_agent_name (case-insensitive partial match) | | `status` | enum | No | Filter by status. submitted=new, accepted=agreed, active=in progress, closed=transaction closed, paid=fee collected, declined=rejected, expired=timed out (default: all) · One of: `all`, `submitted`, `accepted`, `active`, `closed`, `paid`, `declined`, `expired` | | `referral_type` | enum | No | Filter by referral type (default: all) · One of: `all`, `buyer`, `seller`, `both` | | `date_from` | string | No | Show referrals submitted on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show referrals submitted on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived referrals (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "Show me all my active referrals and which agents are working them." - "List referrals I submitted this quarter that have not been paid yet." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # referrals_restore > Restore a previously archived referral back to active views. Restore a previously archived referral back to active views. Reverses the effect of [`referrals_archive`](/tools/referrals/referrals_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referral_id` | string | Yes | UUID of the referral to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived Garcia referral, that deal is back on again." - "Unarchive my referral to Joy Lin so I can keep tracking it." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # referrals_stats > Get aggregate statistics for referrals: total count, counts by status, total fees earned, total fees pending. Get aggregate statistics for referrals: total count, counts by status, total fees earned, total fees pending. Use this for dashboard metrics. Does NOT return individual records - use [`referrals_list`](/tools/referrals/referrals_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How much referral income have I earned year to date, and what is pending?" - "Give me a breakdown of my referrals by status for the last three months." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # referrals_update > Update an existing referral's information or status. ```text Update an existing referral's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: status change (submitted->accepted->active->closed->paid), add financial details, link escrow. Returns the updated referral record. ``` ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `referral_id` | string | Yes | UUID of the referral to update (required). Use [`referrals_list`](/tools/referrals/referrals_list) to find. · Format: UUID | | `referred_name` | string | No | Updated referred person name | | `referred_email` | string | No | Updated referred person email | | `referred_phone` | string | No | Updated referred person phone | | `referred_lead_id` | string | No | Updated lead ID · Format: UUID | | `referred_contact_id` | string | No | Updated contact ID · Format: UUID | | `referring_agent_name` | string | No | Updated referring agent name | | `referring_agent_email` | string | No | Updated referring agent email | | `receiving_agent_id` | string | No | Updated receiving agent ID · Format: UUID | | `receiving_agent_name` | string | No | Updated receiving agent name | | `receiving_agent_email` | string | No | Updated receiving agent email | | `referral_type` | enum | No | Updated referral type · One of: `buyer`, `seller`, `both` | | `referral_source` | string | No | Updated referral source | | `notes` | string | No | Updated notes | | `status` | enum | No | Updated status · One of: `submitted`, `accepted`, `active`, `closed`, `paid`, `declined`, `expired` | | `referral_fee_percentage` | number | No | Updated fee percentage · Max: 100 · Min: 0 | | `transaction_amount` | number | No | Transaction amount when closed · Min: 0 | | `referral_fee_amount` | number | No | Actual referral fee amount · Min: 0 | | `escrow_id` | string | No | Link to an escrow when the referral leads to a transaction · Format: UUID | | `care_status` | enum | No | Care status for tracking · One of: `no_status`, `cared`, `needs_care`, `didnt_care` | ## Example prompts - "Mark the Garcia referral as closed with a 450000 transaction amount." - "Update my referral to Dana Cole to paid, the fee check came in." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Rental applications > Receive and process rental applications for the property-management vertical. Receive and process rental applications for the property-management vertical. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`rental_applications_list`](/tools/rental-applications/rental_applications_list) | Read-only | Search rental applications by decision, applicant, listing, or free-text query. | | [`rental_applications_get`](/tools/rental-applications/rental_applications_get) | Read-only | Get full details of one rental application by UUID. | | [`rental_applications_approve`](/tools/rental-applications/rental_applications_approve) | Updates data | Mark a rental application as approved. | | [`rental_applications_convert_to_lease`](/tools/rental-applications/rental_applications_convert_to_lease) | Creates data | Atomically approve (if pending) and create a lease from an approved application. | --- # rental_applications_approve > Mark a rental application as approved. Mark a rental application as approved. Stamps reviewed_at=NOW. Idempotent — calling on an already-approved application is a no-op. Cannot approve a withdrawn application (will error). Does NOT create a lease — call [`rental_applications_convert_to_lease`](/tools/rental-applications/rental_applications_convert_to_lease) for that atomic flow. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `application_id` | string | Yes | UUID of the application to approve · Format: UUID | | `reason` | string | No | Optional approval note for audit trail (e.g., "Strong credit and income verified") · Max length: 2000 | ## Example prompts - "Approve Tia Moreno's application, her credit and income both checked out." - "Mark the Birchwood Ln application approved with a note that employment was verified." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # rental_applications_convert_to_lease > Atomically approve (if pending) and create a lease from an approved application. Atomically approve (if pending) and create a lease from an approved application. Links the new lease to the application via rental_application_id. The new lease automatically inherits the application's listing and brokerage/team assignment. Cannot convert denied or withdrawn applications. Returns the new lease record. This is the canonical "I've decided to sign this tenant" action. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `application_id` | string | Yes | UUID of the application to convert · Format: UUID | | `lease_start` | string | Yes | Lease start date (ISO format, YYYY-MM-DD) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `lease_end` | string | Yes | Lease end date (ISO format, YYYY-MM-DD) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `monthly_rent` | number | Yes | Monthly rent amount · Min: 0 | | `security_deposit` | number | No | Security deposit amount · Min: 0 | | `lease_type` | enum | No | Lease type (default: residential) · One of: `residential`, `commercial`, `short_term` | | `landlord_contact_id` | string | No | UUID of the landlord contact · Format: UUID | | `renter_client_id` | string | No | UUID of the renter client. If not provided, the agent should create a client with client_type=tenant first via [`clients_create`](/tools/clients/clients_create) and link it here. · Format: UUID | | `commission_amount` | number | No | Agent commission for this lease · Min: 0 | | `notes` | string | No | Lease notes · Max length: 10000 | ## Example prompts - "Sign Tia Moreno as the tenant, twelve month lease at 2400 a month starting July 1." - "Convert the approved Birchwood Ln application into a lease with a 2400 deposit." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # rental_applications_get > Get full details of one rental application by UUID. Get full details of one rental application by UUID. Returns all application fields including credit_score, monthly_income, employment_status, background_check_status, eviction_history (sensitive — caller must sanitize before AI use). Returns decision, decision_reason, reviewed_at for audit trail. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `application_id` | string | Yes | UUID of the rental application · Format: UUID | ## Example prompts - "Show me the full application for 412 Birchwood Ln, including credit score and income." - "Pull up Tia Moreno's application details before I make a decision." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # rental_applications_list > Search rental applications by decision, applicant, listing, or free-text query. Search rental applications by decision, applicant, listing, or free-text query. Returns id, listing_id, applicant_contact_id, decision, submitted_at, reviewed_at. Use decision='pending' or 'reviewing' to find applications needing a decision. Sensitive financial fields (credit_score, monthly_income) are NOT returned in list mode — fetch with [`rental_applications_get`](/tools/rental-applications/rental_applications_get) when needed for decision context. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Free-text search (reserved — use decision/applicant/listing filters for now) | | `decision` | enum | No | Filter by decision status. pending=not yet reviewed, reviewing=in progress, approved/denied=decided, withdrawn=applicant pulled out · One of: `pending`, `reviewing`, `approved`, `denied`, `withdrawn` | | `applicant_contact_id` | string | No | Filter to applications by a specific applicant · Format: UUID | | `listing_id` | string | No | Filter to applications for a specific listing · Format: UUID | | `limit` | number | No | Maximum results (default: 25) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination · Min: 0 | ## Example prompts - "Which rental applications are still pending a decision on my Birchwood Ln listing?" - "List all applications from Tia Moreno across my rental listings." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Reporting & insights > Production reports, lead-source ROI, anniversaries, hot-lead lists, buyer matching, and close-probability estimates. Production reports, lead-source ROI, anniversaries, hot-lead lists, buyer matching, and close-probability estimates. ## Tools (7) | Tool | Type | What it does | | --- | --- | --- | | [`get_production_report`](/tools/reporting/get_production_report) | Read-only | Get a production report showing volume, units closed, and commission earned for a time period. | | [`get_lead_source_roi`](/tools/reporting/get_lead_source_roi) | Read-only | Get lead conversion data grouped by source. | | [`get_client_anniversaries`](/tools/reporting/get_client_anniversaries) | Read-only | Get clients with upcoming home purchase anniversaries for sphere marketing. | | [`get_hot_leads`](/tools/reporting/get_hot_leads) | Read-only | Get active leads sorted by status priority and recency for prospecting. | | [`suggest_next_action`](/tools/reporting/suggest_next_action) | Read-only | Get data-driven suggestions for next steps. | | [`match_buyers_to_listings`](/tools/reporting/match_buyers_to_listings) | Read-only | Find buyer clients whose saved price range overlaps with a listing's price (within 10%). | | [`predict_close_probability`](/tools/reporting/predict_close_probability) | Read-only | Estimate close likelihood for an escrow based on simple heuristics: contingency removal status and days to closing date. | --- # get_client_anniversaries > Get clients with upcoming home purchase anniversaries for sphere marketing. Get clients with upcoming home purchase anniversaries for sphere marketing. A proven touchpoint for staying in contact with past clients and generating referrals. Returns: client name, property address, purchase date, anniversary date, years since purchase. Send an anniversary card or call to maintain the relationship. Also useful for triggering annual home value updates or refinance discussions. Results sorted by anniversary date. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_ahead` | number | No | Look ahead this many days for upcoming anniversaries. Default: 30 days gives time to prepare cards. · Max: 90 · Min: 1 | | `include_past` | number | No | Include anniversaries from past N days for recently missed ones. Default: 7 days. · Max: 30 · Min: 0 | ## Example prompts - "Which past clients have home purchase anniversaries coming up in the next 30 days?" - "Pull anniversaries for the next two weeks so I can mail cards, plus any missed last week." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_hot_leads > Get active leads sorted by status priority and recency for prospecting. Get active leads sorted by status priority and recency for prospecting. Returns leads in priority order: qualified first, then contacted, then new — with most recent first within each group. Use this to build a daily call list. Note: this is a simple sort by status and date, not a predictive scoring model. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `min_score` | number | No | Minimum score 0-100 to include. Default: 50. Use 70+ for hot leads only, 0 for all leads ranked. · Max: 100 · Min: 0 | | `lead_type` | enum | No | Filter by lead type. buyer=buying clients, seller=listing prospects, both=investor/dual leads, all=no filter. Default: all. · One of: `buyer`, `seller`, `both`, `all` | | `limit` | number | No | Maximum results. Default: 10. Your daily call list should be 10-20 leads. · Max: 100 · Min: 1 | ## Example prompts - "Build me a call list of my ten hottest buyer leads for this morning." - "Show qualified seller leads scoring 70 or above so I can prioritize listing appointments." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_lead_source_roi > Get lead conversion data grouped by source. Get lead conversion data grouped by source. Returns for each source: total leads, converted count, and conversion rate. Sources include: referral, zillow, realtor.com, open_house, sign_call, website, social_media, etc. Note: cost per lead and ROI are not tracked — only conversion rates from existing data. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period to analyze. Longer periods give more reliable conversion data. Default: ytd. · One of: `ytd`, `last_6_months`, `last_year`, `all_time` | | `min_leads` | number | No | Minimum leads to include a source in results. Use 5-10 for meaningful averages. Default: 1. · Max: 100 · Min: 1 | ## Example prompts - "Which lead sources convert best for me over the last six months?" - "Compare conversion rates by source year to date, only counting sources with five-plus leads." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_production_report > Get a production report showing volume, units closed, and commission earned for a time period. Get a production report showing volume, units closed, and commission earned for a time period. Essential for tracking business performance and setting goals. Returns: total closed volume, units closed, gross commission, average deal size, and comparison to prior period. Use period parameter for standard ranges (ytd, mtd, qtd) or custom for specific date ranges. group_by allows breakdown by month/quarter to see trends. include_pending adds pipeline value for forecasting. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for the report. ytd=year to date (most common), mtd=month to date, qtd=quarter to date, custom=use start_date/end_date. Default: ytd. · One of: `ytd`, `mtd`, `qtd`, `last_month`, `last_quarter`, `last_year`, `all_time`, `custom` | | `start_date` | string | No | Start date for custom period. Required when period="custom". (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `end_date` | string | No | End date for custom period. Required when period="custom". (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `include_pending` | boolean | No | Include pending escrows in pipeline forecast. Default: true. Set false for closed-only report. | | `group_by` | enum | No | Group results by time period for trend analysis. none=single total, month=monthly breakdown. Default: none. · One of: `month`, `quarter`, `year`, `none` | ## Example prompts - "How much volume and commission have I closed year to date compared to last year?" - "Break down my closed units and GCI by quarter for 2025, excluding pending escrows." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # match_buyers_to_listings > Find buyer clients whose saved price range overlaps with a listing's price (within 10%). Find buyer clients whose saved price range overlaps with a listing's price (within 10%). Returns the listing details and a list of potential buyer clients filtered by price criteria. Results are filtered by data, not recommended — the agent should independently evaluate suitability for each buyer. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | No | UUID of the buyer client to match. Their saved preferences (budget, beds, baths, areas) are used for matching. · Format: UUID | | `min_price` | number | No | Override client minimum price. Useful for expanding search if few matches. · Min: 0 | | `max_price` | number | No | Override client maximum price. Useful for stretching budget for right property. · Min: 0 | | `min_beds` | number | No | Minimum bedrooms filter. · Max: 10 · Min: 0 | | `min_baths` | number | No | Minimum bathrooms filter. · Max: 10 · Min: 0 | | `property_types` | array of enum | No | Property types to search. Default: all types client is interested in. · Values: `single_family`, `condo`, `townhouse`, `multi_family`, `land`, `commercial` | | `cities` | array of strings | No | Cities to search in. Default: cities from client preferences. | | `limit` | number | No | Maximum results. Default: 10. Use 20-30 for comprehensive showing tour. · Max: 50 · Min: 1 | ## Example prompts - "Which active listings fit the Garcias budget and their three-bedroom requirement in Bakersfield?" - "Stretch Tom and Priya's max price to 525k and rerun their listing matches." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # predict_close_probability > Estimate close likelihood for an escrow based on simple heuristics: contingency removal status and days to closing date. Estimate close likelihood for an escrow based on simple heuristics: contingency removal status and days to closing date. Returns a rough percentage (not a statistical model) and the factors used. This is an internal planning heuristic only — do not share the percentage with clients or present it as a factual prediction. The agent should apply their own experience and judgment. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | UUID of the escrow to analyze. Use [`escrows_list`](/tools/escrows/escrows_list) to find ID. · Format: UUID | ## Example prompts - "How likely is the Birchwood Ln escrow to close on time, roughly?" - "Give me a rough close-probability read on the Hammond escrow for my own planning." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # suggest_next_action > Get data-driven suggestions for next steps. Get data-driven suggestions for next steps. Two modes: (1) HOLISTIC — call with NO args to get the top highest-impact next moves across hot leads, closing escrows, and stale clients (use this for prompts like "what's my next move?" or "what should I do next?"). (2) PER-ENTITY — pass entity_type ('lead' | 'client' | 'escrow') AND entity_id to get suggestions scoped to one specific record. Returns suggestions with priority levels and reasons. These are suggestions for internal planning — the licensed agent must exercise independent professional judgment on all actions. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `entity_type` | enum | No | OPTIONAL. Type of entity to analyze. Omit for holistic mode. · One of: `lead`, `client`, `escrow` | | `entity_id` | string | No | OPTIONAL. UUID of the entity to analyze. Omit for holistic mode. If provided, entity_type is also required. · Format: UUID | ## Example prompts - "What's my highest-impact next move right now across leads and escrows?" - "What should I do next on the escrow for 412 Birchwood Ln?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Review requests > Find clients from recently closed escrows, draft a review request email, and send it after your explicit confirmation. Find clients from recently closed escrows, draft a review request email, and send it after your explicit confirmation. ## Tools (3) | Tool | Type | What it does | | --- | --- | --- | | [`list_clients_pending_review_request`](/tools/review-requests/list_clients_pending_review_request) | Read-only | List the agent's closed escrows that are eligible for a Google review request but haven't been asked yet. | | [`draft_review_request_email`](/tools/review-requests/draft_review_request_email) | Read-only | Generate the personalized subject, plain-text body, HTML body, and Google write-review URL for a review request email tied to a specific closed escrow. | | [`send_review_request`](/tools/review-requests/send_review_request) | Creates data + external action | Send a review request email NOW for a specific closed escrow. | --- # draft_review_request_email > Generate the personalized subject, plain-text body, HTML body, and Google write-review URL for a review request email tied to a specific closed escrow. Generate the personalized subject, plain-text body, HTML body, and Google write-review URL for a review request email tied to a specific closed escrow. Read-only — does NOT send. Use this BEFORE [`send_review_request`](/tools/review-requests/send_review_request) so the user can preview and edit the draft in chat. The tool also reports `suppressed: true` (with a reason) if the recipient is on the email suppression list — the assistant should surface that to the user instead of proceeding to send. Any reviewer comment fields present in related data are sanitized to strip protected-characteristic and personal information before any AI processing. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | Identifier of the closed escrow. NOTE: escrow IDs are text identifiers (e.g. "ESC-2026-0042"), not UUIDs. Use [`list_clients_pending_review_request`](/tools/review-requests/list_clients_pending_review_request) or [`escrows_list`](/tools/escrows/escrows_list) to discover it. · Max length: 100 · Min length: 1 | | `tone` | enum | No | Tone variant for the email copy. "warm" is the default in the personalization service; choose explicitly if the user asks for a different feel. · One of: `warm`, `professional`, `casual` | ## Example prompts - "Draft a warm review request email for the Garcias closing so I can preview it." - "Write a professional-tone review ask for escrow ESC-2026-0042 before we send anything." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # list_clients_pending_review_request > List the agent's closed escrows that are eligible for a Google review request but haven't been asked yet. List the agent's closed escrows that are eligible for a Google review request but haven't been asked yet. A closing is eligible when it has a recorded close date within the days_since_close window AND no review request has been sent to the same client for that escrow in the last 60 days. Use when the user asks "who should I ask for a review?", "any closings I forgot to follow up on?", or similar. Returns one entry per pending closing with the data needed to draft a request: escrow_id, client_name, property_address, close_date, days_since_close. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `days_since_close` | integer | No | Window of closings to consider, measured from the actual close-of-escrow date (or the scheduled closing date when no actual date is recorded) up to today. Default 7 — matches the typical post-close cooling-off period used by the auto-send scheduler. · Max: 365 · Min: 1 | ## Example prompts - "Who should I ask for a Google review from my recent closings?" - "Any closings in the last 30 days where I forgot to request a review?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # send_review_request > Send a review request email NOW for a specific closed escrow. Send a review request email NOW for a specific closed escrow. **REQUIRES EXPLICIT USER CONFIRMATION IN CHAT BEFORE EXECUTION.** The assistant MUST first call [`draft_review_request_email`](/tools/review-requests/draft_review_request_email) to show the user the proposed subject + body, and MUST receive an unambiguous "yes, send it" (or equivalent confirmation) from the user before invoking this tool. This is a write tool that sends a real email. The service enforces the email suppression list, blocks repeat sends to the same client for the same escrow within 60 days, and applies the per-user limit of 50 sends per day — the same safeguards enforced by the REST endpoint. Returns `{ message_id, sent_at, status }` on success or a structured `skipped_*` / `error` outcome that explains why the send did not happen. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `escrow_id` | string | Yes | Identifier of the closed escrow. A text identifier (e.g. "ESC-2026-0042"), not a UUID. · Max length: 100 · Min length: 1 | | `template_variant` | enum | No | Tone variant to send. Defaults to the user's saved review-request tone preference (typically "warm"). Pick explicitly when the user asked for a specific tone in their confirmation. · One of: `warm`, `professional`, `casual` | | `custom_body` | string | No | Optional custom body override. The current template does NOT include this text in the sent email — it is recorded on the audit trail only. Pass it when the user dictates exact words, but be aware the recipient still receives the standard template body for now. · Max length: 5000 | ## Example prompts - "Yes, send that review request to the Garcias now, the warm version." - "Go ahead and send the review email for ESC-2026-0107 with the casual tone." ## Safety **Creates data + external action.** Creates a record and triggers an action in a connected external service. --- # Showings > Schedule, track, and log property showings, including buyer feedback. Schedule, track, and log property showings, including buyer feedback. ## Tools (8) | Tool | Type | What it does | | --- | --- | --- | | [`showings_list`](/tools/showings/showings_list) | Read-only | Search for property showings by address, status, type, or date range. | | [`showings_get`](/tools/showings/showings_get) | Read-only | Get full details of a showing by UUID, including listing and contact information. | | [`showings_create`](/tools/showings/showings_create) | Creates data | Schedule a new property showing. | | [`showings_update`](/tools/showings/showings_update) | Updates data | Update an existing showing's information or status. | | [`showings_stats`](/tools/showings/showings_stats) | Read-only | Get aggregate statistics for showings: total count, upcoming count, counts by status, counts by type. | | [`showings_archive`](/tools/showings/showings_archive) | Updates data | Archive a showing to hide it from active views without deleting it. | | [`showings_restore`](/tools/showings/showings_restore) | Updates data | Restore a previously archived showing back to active views. | | [`showings_delete`](/tools/showings/showings_delete) | Irreversible | Permanently delete a showing. | --- # showings_archive > Archive a showing to hide it from active views without deleting it. Archive a showing to hide it from active views without deleting it. Archived showings can be restored later with [`showings_restore`](/tools/showings/showings_restore). Use this for past showings you want to declutter from your dashboard. Returns the archived record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `showing_id` | string | Yes | UUID of the showing to archive. Use [`showings_list`](/tools/showings/showings_list) to find. · Format: UUID | ## Example prompts - "Archive that cancelled showing at 76 Pinehurst Ct to clean up my dashboard." - "Tidy up by archiving last month's completed showing at 412 Birchwood Ln." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # showings_create > Schedule a new property showing. Schedule a new property showing. WORKFLOW: 1) Optionally use [`listings_list`](/tools/listings/listings_list) to find the listing 2) Optionally use [`contacts_list`](/tools/contacts/contacts_list) to find the contact 3) Create the showing with address, date, and time. Returns the created showing with ID. Required fields: address, showing_date, showing_time. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | Property address for the showing (required) | | `showing_date` | string | Yes | Date of the showing (required) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `showing_time` | string | Yes | Time of the showing in HH:MM format (required), e.g. "14:00" | | `listing_id` | string | No | UUID of the associated listing (optional). Use [`listings_list`](/tools/listings/listings_list) to find. · Format: UUID | | `contact_id` | string | No | UUID of the associated contact (optional). Use [`contacts_list`](/tools/contacts/contacts_list) to find. · Format: UUID | | `appointment_id` | string | No | UUID of the associated appointment (optional). Use [`appointments_list`](/tools/appointments/appointments_list) to find. · Format: UUID | | `showing_type` | enum | No | Type of showing (default: first_showing) · One of: `first_showing`, `follow_up`, `final_walkthrough`, `inspection`, `appraisal` | | `access_type` | string | No | Access type, e.g. "lockbox", "agent_present", "owner_present" | | `lockbox_code` | string | No | Lockbox code for property access | | `access_notes` | string | No | Notes about property access instructions | | `showing_status` | enum | No | Initial status (default: pending) · One of: `pending`, `confirmed`, `completed`, `cancelled`, `no_show` | ## Example prompts - "Schedule a first showing at 412 Birchwood Ln this Saturday at 2pm for the Garcias." - "Book a final walkthrough at 1530 Kern River Rd on June 27 at 10am, lockbox access." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # showings_delete > Permanently delete a showing. Permanently delete a showing. This is IRREVERSIBLE - the showing will be hidden from all views, including archived. The showing must be archived first. For normal cleanup, use [`showings_archive`](/tools/showings/showings_archive) instead which is reversible. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `showing_id` | string | Yes | UUID of the showing to delete. Must be archived first. Use [`showings_get`](/tools/showings/showings_get) to verify. · Format: UUID | ## Example prompts - "Permanently delete that duplicate showing entry for 88 Calloway Dr." - "Remove the old test showing at 12 Maple Ct for good, but double-check with me before deleting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it. --- # showings_get > Get full details of a showing by UUID, including listing and contact information. Get full details of a showing by UUID, including listing and contact information. Returns address, date/time, status, type, access info, feedback, notes, and joined listing + contact details. If you don't have the UUID, use [`showings_list`](/tools/showings/showings_list) first. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `showing_id` | string | Yes | UUID of the showing. Use [`showings_list`](/tools/showings/showings_list) to find IDs. · Format: UUID | ## Example prompts - "Pull full details on tomorrow's showing at 88 Calloway Dr, including access notes." - "What's the lockbox info and client feedback for the Birchwood Ln showing?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # showings_list > Search for property showings by address, status, type, or date range. Search for property showings by address, status, type, or date range. Returns pagination info (total_count, has_more, offset, limit). Results include: id, address, showing_date, showing_time, showing_status, showing_type, access_type, listing address/photo, contact name, agent_notes, created_at. Filter by status (pending/confirmed/completed/cancelled/no_show), type (first_showing/follow_up/final_walkthrough/inspection/appraisal), or date range. For a specific showing by ID, use [`showings_get`](/tools/showings/showings_get) instead. For aggregate stats, use [`showings_stats`](/tools/showings/showings_stats). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Search term - matches against address (case-insensitive partial match) | | `status` | enum | No | Filter by status. pending=awaiting confirmation, confirmed=scheduled, completed=done, cancelled=cancelled, no_show=missed (default: all) · One of: `all`, `pending`, `confirmed`, `completed`, `cancelled`, `no_show` | | `showing_type` | enum | No | Filter by showing type (default: all) · One of: `all`, `first_showing`, `follow_up`, `final_walkthrough`, `inspection`, `appraisal` | | `date_from` | string | No | Show showings on or after this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `date_to` | string | No | Show showings on or before this date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `archived` | boolean | No | Include archived showings (default: false) | | `limit` | number | No | Maximum results (default: 50, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "List my confirmed showings between Friday and Sunday this week." - "Pull up any pending showings at 412 Birchwood Ln awaiting confirmation." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # showings_restore > Restore a previously archived showing back to active views. Restore a previously archived showing back to active views. Reverses the effect of [`showings_archive`](/tools/showings/showings_archive). Returns the restored record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `showing_id` | string | Yes | UUID of the showing to restore. Must be currently archived. · Format: UUID | ## Example prompts - "Restore the archived Birchwood Ln showing, the buyers want a second look after all." - "Bring back the Pinehurst Ct showing I archived last week." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # showings_stats > Get aggregate statistics for showings: total count, upcoming count, counts by status, counts by type. Get aggregate statistics for showings: total count, upcoming count, counts by status, counts by type. Use this for dashboard metrics. Does NOT return individual records - use [`showings_list`](/tools/showings/showings_list) for that. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `period` | enum | No | Time period for stats (default: 1_month) · One of: `1_day`, `1_week`, `1_month`, `3_months`, `1_year`, `ytd`, `all` | ## Example prompts - "How many showings did I run this month, broken down by status?" - "Give me my year-to-date showing counts and how many are upcoming." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # showings_update > Update an existing showing's information or status. Update an existing showing's information or status. Only provided fields are updated - omitted fields remain unchanged. Common uses: reschedule (update date/time), status change (pending→confirmed→completed), add feedback/notes. Returns the updated showing record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `showing_id` | string | Yes | UUID of the showing to update (required). Use [`showings_list`](/tools/showings/showings_list) to find. · Format: UUID | | `address` | string | No | Updated property address | | `showing_date` | string | No | Updated showing date (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `showing_time` | string | No | Updated showing time in HH:MM format | | `listing_id` | string | No | Updated listing ID · Format: UUID | | `contact_id` | string | No | Updated contact ID · Format: UUID | | `appointment_id` | string | No | Updated appointment ID · Format: UUID | | `showing_type` | enum | No | Updated showing type · One of: `first_showing`, `follow_up`, `final_walkthrough`, `inspection`, `appraisal` | | `access_type` | string | No | Updated access type | | `lockbox_code` | string | No | Updated lockbox code | | `access_notes` | string | No | Updated access notes | | `showing_status` | enum | No | Updated status · One of: `pending`, `confirmed`, `completed`, `cancelled`, `no_show` | | `client_interest_level` | number | No | Client interest level (1-10) · Max: 10 · Min: 1 | | `agent_notes` | string | No | Agent notes about the showing | | `client_feedback` | string | No | Client feedback about the property | | `confirmation_number` | string | No | Showing confirmation number | ## Example prompts - "Reschedule the Garcias showing at Birchwood Ln to Sunday at 3:30pm." - "Mark yesterday's Calloway Dr showing completed and log their interest level at 8." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # Dashboard & stats > At-a-glance counters for your CRM — answers to "how many X do I have?" questions. At-a-glance counters for your CRM — answers to "how many X do I have?" questions. ## Tools (2) | Tool | Type | What it does | | --- | --- | --- | | [`get_dashboard_stats`](/tools/stats/get_dashboard_stats) | Read-only | Get comprehensive statistics for all CRM entities in one call. | | [`get_contact_stats`](/tools/stats/get_contact_stats) | Read-only | Get detailed statistics specifically for contacts including breakdown by contact type. | --- # get_contact_stats > Get detailed statistics specifically for contacts including breakdown by contact type. Get detailed statistics specifically for contacts including breakdown by contact type. Returns: total contacts, new contacts this month, counts by type (vendor, attorney, lender, inspector, escrow_officer, title_rep, etc.). Use this for contact directory health checks or to see distribution of your professional network. More detailed than [`get_dashboard_stats`](/tools/stats/get_dashboard_stats) for contacts. For individual contact details, use [`contacts_list`](/tools/contacts/contacts_list) instead. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Break down my contact directory by type, lenders, inspectors, title reps, all of it." - "How many new contacts did I add this month, and what's the type distribution?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # get_dashboard_stats > Get comprehensive statistics for all CRM entities in one call. Get comprehensive statistics for all CRM entities in one call. Perfect for dashboard displays and health checks. Returns for each entity type: total count, active count, recent additions. Entities covered: contacts (total, new this month), escrows (by status: active/pending/closed/cancelled), clients (total, active, by type), leads (by status: new/contacted/qualified), listings (by status: active/pending/sold), appointments (today count, this week count). Use entity parameter to filter to specific type, or "all" for complete overview. This is a read-only aggregation — no individual records returned. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `entity` | enum | No | Which entity to get stats for. "all" returns stats for every entity type. Default: all. · One of: `all`, `contacts`, `escrows`, `clients`, `leads`, `listings`, `appointments` | ## Example prompts - "Give me a quick overview of my whole pipeline, leads, listings, escrows, everything." - "How many active escrows and new leads do I have right now?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Tenancies > Manage tenancy records, including delinquency lookups, for the property-management vertical. Manage tenancy records, including delinquency lookups, for the property-management vertical. ## Tools (7) | Tool | Type | What it does | | --- | --- | --- | | [`tenancies_list`](/tools/tenancies/tenancies_list) | Read-only | Search the user's property-managed tenancies. | | [`tenancies_get`](/tools/tenancies/tenancies_get) | Read-only | Get full details of one tenancy by UUID — includes management fee, rent collection state, inspection cadence, linked lease_id and owner_client_id. | | [`tenancies_create`](/tools/tenancies/tenancies_create) | Creates data | Start managing a lease. | | [`tenancies_mark_rent_collected`](/tools/tenancies/tenancies_mark_rent_collected) | Updates data | Stamp rent as collected for a tenancy. | | [`tenancies_record_inspection`](/tools/tenancies/tenancies_record_inspection) | Updates data | Record a property inspection. | | [`tenancies_delinquent`](/tools/tenancies/tenancies_delinquent) | Read-only | List active and vacating tenancies where rent is currently late (rent_current=FALSE). | | [`tenancies_stats`](/tools/tenancies/tenancies_stats) | Read-only | Get aggregate property-management portfolio stats: total tenancies, by_status breakdown, assets_under_management_monthly (sum of monthly_rent from the linked leases of active tenancies — the recurring revenue you're managing), delinquent_count, inspections_due_in_30_days. | --- # tenancies_create > Start managing a lease. Start managing a lease. Requires lease_id (the UUID of an existing lease). Use when taking over management of a signed lease outside the consultation flow — for the common case of converting a signed PM consultation into a tenancy, prefer [`pm_consultations_convert_to_tenancy`](/tools/pm-consultations/pm_consultations_convert_to_tenancy), which atomically signs + creates. Defaults management_fee_pct=10.00, rent_collection_day=1, rent_current=TRUE, status='active'. Returns the created tenancy record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `lease_id` | string | Yes | UUID of the lease to start managing (required) · Format: UUID | | `owner_client_id` | string | No | UUID of the owner client (landlord) who hired the PM · Format: UUID | | `pm_consultation_id` | string | No | UUID of the consultation that led to this tenancy (optional; for audit trail) · Format: UUID | | `management_fee_pct` | number | No | Management fee percentage (default 10.00) · Max: 100 · Min: 0 | | `rent_collection_day` | number | No | Day of month rent is collected (default 1) · Max: 31 · Min: 1 | | `status` | enum | No | Initial status (default: active) · One of: `active`, `vacating`, `vacant`, `terminated` | | `notes` | string | No | Free-text notes on the tenancy (operational details, landlord preferences, etc.) · Max length: 10000 | ## Example prompts - "Start managing the new lease at 219 Dover St with an 8 percent fee." - "Set up management for the Whitfield lease, rent collected on the 5th each month." ## Safety **Creates data.** Creates a new record in your CRM and may trigger notifications or webhooks. Running it twice creates a duplicate, so Claude runs it once per request. --- # tenancies_delinquent > List active and vacating tenancies where rent is currently late (rent_current=FALSE). List active and vacating tenancies where rent is currently late (rent_current=FALSE). Ordered by last_rent_collected_at ascending (longest-overdue first) with NULLs first (never-collected). This is the primary tool for the "who do I need to chase on rent" question. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Who is behind on rent across my managed properties right now?" - "Show me the longest-overdue delinquent tenancies first so I can start chasing rent." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # tenancies_get > Get full details of one tenancy by UUID — includes management fee, rent collection state, inspection cadence, linked lease_id and owner_client_id. Get full details of one tenancy by UUID — includes management fee, rent collection state, inspection cadence, linked lease_id and owner_client_id. Use [`tenancies_list`](/tools/tenancies/tenancies_list) first if you don't have the UUID. Returns the complete tenancy record. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `tenancy_id` | string | Yes | UUID of the tenancy · Format: UUID | ## Example prompts - "Pull the full management details on the tenancy at 905 Sycamore Ave." - "When is the next inspection due for the Hendersons tenancy, and what's the fee?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # tenancies_list > Search the user's property-managed tenancies. Search the user's property-managed tenancies. Returns id, lease_id, status (active/vacating/vacant/terminated), rent_current, management_fee_pct, next_inspection_due_at. Use rent_current=false to find delinquent tenancies (or call [`tenancies_delinquent`](/tools/tenancies/tenancies_delinquent) for a dedicated view). Pagination: total_count + offset + limit. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `query` | string | No | Free-text search (reserved) | | `status` | enum | No | Filter by tenancy lifecycle status · One of: `active`, `vacating`, `vacant`, `terminated` | | `owner_client_id` | string | No | Filter to tenancies owned by a specific client (landlord) · Format: UUID | | `rent_current` | boolean | No | Filter by rent-current flag (false = delinquent) | | `limit` | number | No | Maximum results (default: 25, max: 500) · Max: 500 · Min: 1 | | `offset` | number | No | Skip N results for pagination (default: 0) · Min: 0 | ## Example prompts - "List all my active managed tenancies with their management fee percentages." - "Show vacating tenancies in Marcus Bell's rental portfolio." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # tenancies_mark_rent_collected > Stamp rent as collected for a tenancy. Stamp rent as collected for a tenancy. Sets last_rent_collected_at (default: today) and flips rent_current to TRUE. Call this after confirming rent receipt (ACH settlement, check cleared, etc.). ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `tenancy_id` | string | Yes | UUID of the tenancy · Format: UUID | | `collected_at` | string | No | Date rent was collected (default: today) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | ## Example prompts - "Mark June rent collected for the Sycamore Ave tenancy, the ACH settled today." - "The Dover St tenant's check cleared yesterday, stamp their rent as received." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # tenancies_record_inspection > Record a property inspection. Record a property inspection. Stamps last_inspection_at and (optionally) sets the next_inspection_due_at. Inspections are typically on a 6-month cadence for active tenancies. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `tenancy_id` | string | Yes | UUID of the tenancy · Format: UUID | | `inspected_at` | string | No | Date of inspection (default: today) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | | `next_due_at` | string | No | When the next inspection should occur (optional — typically +6 months) (Format: YYYY-MM-DD) · Format: Date (YYYY-MM-DD) | ## Example prompts - "Log today's inspection at 905 Sycamore Ave and set the next one for December." - "Record the June 9 walkthrough for the Dover St tenancy, next due in six months." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # tenancies_stats > Get aggregate property-management portfolio stats: total tenancies, by_status breakdown, assets_under_management_monthly (sum of monthly_rent from the linked leases of active tenancies — the recurring revenue you're managing), delinquent_count, inspections_due_in_30_days. Get aggregate property-management portfolio stats: total tenancies, by_status breakdown, assets_under_management_monthly (sum of monthly_rent from the linked leases of active tenancies — the recurring revenue you're managing), delinquent_count, inspections_due_in_30_days. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "What's my total monthly rent under management and how many doors am I managing?" - "How many tenancies are delinquent, and how many inspections come due in 30 days?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Utilities > Helper lookups used before other operations: verified address lookup and parcel/APN lookup. Helper lookups used before other operations: verified address lookup and parcel/APN lookup. ## Tools (2) | Tool | Type | What it does | | --- | --- | --- | | [`lookup_parcel`](/tools/utility/lookup_parcel) | Read-only | Look up cached parcel/county-tax-assessor data by APN+county or property address. | | [`lookup_address`](/tools/utility/lookup_address) | External lookup | Look up a property address using Google Places API to get structured, verified address data. | --- # lookup_address > Look up a property address using Google Places API to get structured, verified address data. Look up a property address using Google Places API to get structured, verified address data. > [!IMPORTANT] > Use this BEFORE creating escrows or listings to ensure accurate address data. Returns: formatted address, street number, street name, city, state, zip code, county (important for transfer tax calculations), latitude/longitude coordinates (for mapping), and place_id. If the address is ambiguous, returns multiple suggestions — pick the correct one. Common issues: apartment numbers may need manual addition, PO boxes are rejected, new construction may not be found. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | Yes | The property address to look up. Be as specific as possible. Examples: "123 Main St, Bakersfield, CA", "456 Oak Ave Unit 12, Los Angeles 90001". Include city and state for best results. · Max length: 200 · Min length: 5 | ## Example prompts - "Verify the exact address and county for 7421 Stockdale Hwy, Bakersfield before I open escrow." - "What county is 88 Calloway Dr in? I need it for transfer tax." ## Safety **External lookup.** A read-only lookup against a service outside ActuallyCare. It may take a few seconds and is subject to that service’s availability. --- # lookup_parcel > Look up cached parcel/county-tax-assessor data by APN+county or property address. Look up cached parcel/county-tax-assessor data by APN+county or property address. Returns assessed value, annual property tax, county, owner name (sanitized for AI use). Cache-only read — does not trigger a paid vendor lookup. Use BEFORE generating a net sheet to enrich seller proceeds estimates with verified tax basis. Returns null if the property has not been previously cached. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `apn` | string | No | Assessor Parcel Number (e.g. "123-456-789"). Pair with `county` for the most precise lookup. · Max length: 50 · Min length: 1 | | `county` | string | No | County name (e.g. "Riverside"). Required when looking up by APN. · Max length: 100 · Min length: 1 | | `address` | string | No | Property address. Used when APN is unknown. Lowercased and whitespace-normalized before lookup. · Max length: 250 · Min length: 5 | | `state` | string | No | 2-letter US state code. Defaults to "CA" if omitted. · Max length: 2 · Min length: 2 | ## Example prompts - "Look up the assessed value and annual property tax for 412 Birchwood Ln." - "Check cached assessor data for APN 412-031-022 in Kern County before I build the net sheet." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Verticals & roles > Read-only discovery of platform verticals and role definitions. Read-only discovery of platform verticals and role definitions. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`vertical_schema_get`](/tools/verticals/vertical_schema_get) | Read-only | Get the metadata schema for a specific vertical and entity type (deal, lead, client, appointment). | | [`verticals_list`](/tools/verticals/verticals_list) | Read-only | List all industry verticals available in the ActuallyCare platform. | | [`roles_list`](/tools/verticals/roles_list) | Read-only | List all user roles in the platform role registry. | | [`role_lookup`](/tools/verticals/role_lookup) | Read-only | Get full details about a specific user role by its key. | --- # role_lookup > Get full details about a specific user role by its key. Get full details about a specific user role by its key. Returns the role's vertical mapping, scope level, description, and whether it allows login. Use this to understand what a specific role key means and what vertical/authorization tier it belongs to. Works for both active and legacy (deprecated) roles. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `role_key` | string | Yes | The role key to look up (e.g., "broker", "plumber", "home_inspector", "transaction_coordinator") | ## Example prompts - "What exactly does the transaction coordinator role allow, and what scope level is it?" - "Is the plumber role loginable, and what vertical is it mapped to?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # roles_list > List all user roles in the platform role registry. List all user roles in the platform role registry. Use to understand what a role can do before assigning it, or to enumerate the roles available in a vertical. Each role maps to exactly one vertical and has a scope_level (admin, brokerage, team, standard) that determines its authorization tier. Filterable by vertical key (e.g., "inspection" returns 16 roles) or scope_level (e.g., "brokerage" returns broker-level roles). Returns role key, vertical_key, vertical_label, label, description, scope_level, and is_loginable. There are roughly 80 active roles across 12 verticals. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `vertical` | string | No | Filter by vertical key (e.g., "real_estate", "inspection", "home_services", "lending"). Leave empty for all roles. | | `scope_level` | enum | No | Filter by scope level. "admin" = full system access, "brokerage" = brokerage-wide, "team" = team-wide, "standard" = own data. · One of: `admin`, `brokerage`, `team`, `standard` | ## Example prompts - "List all brokerage-level roles across the platform." - "What roles exist in the inspection vertical?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # vertical_schema_get > Get the metadata schema for a specific vertical and entity type (deal, lead, client, appointment). Get the metadata schema for a specific vertical and entity type (deal, lead, client, appointment). Returns field definitions with types, labels, validation rules, and select options. Use this to understand what structured metadata fields are available for a vertical before creating or updating deals. Example: get lending deal schema to see loan_type, interest_rate, ltv_ratio fields. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `vertical_key` | string | Yes | Vertical key (e.g., lending, inspection, home_services) | | `entity_type` | enum | Yes | Entity type to get schema for · One of: `deal`, `lead`, `client`, `appointment` | ## Example prompts - "What metadata fields are available on lending deals, like loan type and rate?" - "Show me the structured fields for inspection appointments before I create one." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # verticals_list > List all industry verticals available in the ActuallyCare platform. List all industry verticals available in the ActuallyCare platform. Each vertical represents an industry segment (real_estate, lending, inspection, home_services, etc.) with its own pipeline labels. Returns vertical key, label, description, icon, pipeline_labels (entity naming by stage), and role_count. Use this to understand what industries the platform supports and how each pipeline stage is labeled per vertical. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Which industry verticals does the platform support, and how are their pipelines labeled?" - "List every vertical with its pipeline stage names and role counts." ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # Websites > Agent personal website edit-publish-revert cycle, with Fair Housing, DRE, and IDX guardrails enforced server-side. Agent personal website edit-publish-revert cycle, with Fair Housing, DRE, and IDX guardrails enforced server-side. ## Tools (4) | Tool | Type | What it does | | --- | --- | --- | | [`website_get`](/tools/websites/website_get) | Read-only | Get the current user's personal website status and settings. | | [`website_edit`](/tools/websites/website_edit) | Creates data + external action | Edit the agent's personal website using natural language. | | [`website_publish`](/tools/websites/website_publish) | Updates data | Publish the current draft of the agent's website to make it live. | | [`website_revert`](/tools/websites/website_revert) | Irreversible | Revert the agent's website draft to the last published version. | --- # website_edit > Edit the agent's personal website using natural language. Edit the agent's personal website using natural language. Describe what you want to change (colors, layout, text, sections) and the AI will update the HTML/CSS in real time. Examples: "Change the hero background to dark blue", "Add a section about my specialties", "Make the contact form more prominent". The edit is applied to the draft — use [`website_publish`](/tools/websites/website_publish) to make it live. Guardrails enforce Fair Housing, DRE, and IDX compliance automatically. Returns the updated HTML, diff summary, and any guardrail flags. ## Parameters | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `prompt` | string | Yes | Natural language description of the edit to make. Be specific about what to change and how. · Max length: 2000 | ## Example prompts - "Change my website hero background to dark blue and make the contact form bigger." - "Add a section to my site about my Bakersfield luxury listing specialty." ## Safety **Creates data + external action.** Creates a record and triggers an action in a connected external service. --- # website_get > Get the current user's personal website status and settings. Get the current user's personal website status and settings. Returns: id, templateId, status (draft/published/suspended), subdomain, metaTitle, metaDescription, approvalRequired, approvalStatus, publishedAt, createdAt. Use this to check if the agent has a website and its current state before performing other website operations. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Is my personal website published yet, and what subdomain is it on?" - "Check my website status, did the brokerage approve it for publishing?" ## Safety **Read-only.** This tool never changes your data — it only looks things up. Safe to run anytime. --- # website_publish > Publish the current draft of the agent's website to make it live. Publish the current draft of the agent's website to make it live. The website will be accessible at username.actuallycare.com. Only works if there is draft content to publish. If the brokerage requires approval, the website must be approved first before publishing. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "My draft looks good, push my website live at my subdomain." - "Publish the latest draft of my agent site so visitors see the new bio." ## Safety **Updates data.** Changes an existing record. The change is visible immediately in the app and can be edited back. --- # website_revert > Revert the agent's website draft to the last published version. Revert the agent's website draft to the last published version. Use this to undo all draft changes and restore the live version. Only works if there is a published version to revert to. ## Parameters _This tool takes no parameters — just ask._ ## Example prompts - "Scrap my draft changes and roll my website back to the live version." - "Undo everything in my site draft, but double-check with me before reverting." ## Safety **Irreversible.** This action cannot be undone. Claude should always confirm with you before running it.