← Blog

How to Build a Claude-Powered CRM Assistant

2026-10-30 · 5 min read · SubToAPI Team

A Claude-powered CRM assistant is a chat interface wired into your CRM data that can look up contacts, summarize deal history, draft follow-up emails, and log notes — all through natural language instead of forms and dropdowns. You build one by combining Claude's tool use (function calling) with a small set of read/write endpoints into your CRM, plus a streaming chat UI so responses feel instant.

This guide walks through the actual architecture: defining tools Claude can call, handling multi-step tool conversations, keeping CRM writes safe, and shipping a chat interface your sales team will actually use.

What the assistant should actually do

Before writing code, scope the assistant narrowly. The most useful CRM assistants handle a small set of high-frequency tasks well, rather than trying to replace the whole CRM:

Each of these maps to one or more tool calls Claude makes against your CRM's API (or a thin wrapper around it).

Architecture overview

A minimal setup looks like this:

  1. Frontend chat UI — sends user messages, renders streamed replies.
  2. Backend orchestrator — holds conversation state, calls Claude, executes tool calls, feeds results back.
  3. CRM adapter layer — thin functions that translate tool calls into real CRM API requests (HubSpot, Salesforce, Pipedrive, or your own database).
  4. Claude access layer — the actual model calls. This is where you decide between managing raw API keys yourself or going through a gateway like SubToAPI that gives you a single sub_live_... key, usage metadata per request, and team seats so each rep's usage is tracked separately.

The orchestrator is the only piece with CRM write access — Claude never touches your database directly. It only returns structured tool calls, which your backend validates and executes.

Defining CRM tools

Tool definitions are the core of this project. Keep them specific — vague tools produce vague, unreliable calls.

[
  {
    "name": "get_contact",
    "description": "Fetch a CRM contact by email or name, including recent activity.",
    "input_schema": {
      "type": "object",
      "properties": {
        "query": { "type": "string", "description": "Email or full name" }
      },
      "required": ["query"]
    }
  },
  {
    "name": "log_note",
    "description": "Log a timestamped note on a contact or deal record.",
    "input_schema": {
      "type": "object",
      "properties": {
        "contact_id": { "type": "string" },
        "note": { "type": "string" }
      },
      "required": ["contact_id", "note"]
    }
  },
  {
    "name": "list_stale_deals",
    "description": "List open deals with no activity in the last N days.",
    "input_schema": {
      "type": "object",
      "properties": {
        "days": { "type": "integer", "default": 14 }
      }
    }
  }
]

Write tools should be explicit about scope — log_note only appends, it never deletes or overwrites. If a tool could cause irreversible damage (deleting a contact, changing deal value), either exclude it from the assistant or require a confirmation step in the UI before executing it.

Handling the tool-use loop

Claude's tool use is a loop: you send the message and tool definitions, Claude replies with a tool call, your backend executes it, and you send the result back for Claude to continue. A simplified version:

async function runAssistant(messages, tools) {
  let response = await callClaude(messages, tools);

  while (response.stop_reason === "tool_use") {
    const toolCall = response.content.find(c => c.type === "tool_use");
    const result = await executeCrmTool(toolCall.name, toolCall.input);

    messages.push({ role: "assistant", content: response.content });
    messages.push({
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: toolCall.id,
        content: JSON.stringify(result)
      }]
    });

    response = await callClaude(messages, tools);
  }

  return response;
}

executeCrmTool is where you map get_contact, log_note, and list_stale_deals to real CRM calls, with permission checks tied to the logged-in user. See /docs/tools for how tool definitions and tool_result blocks fit into SubToAPI's Messages API.

Streaming replies in the chat UI

Sales reps expect chat-like responsiveness, not a 5-second spinner. Stream the final text response once tool calls are resolved:

const res = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "claude-sonnet-4",
    stream: true,
    messages,
    tools
  })
});

Details on handling the event stream, including partial tool-use chunks, are in /docs/streaming. If you're just getting started, /docs/quickstart covers authentication and your first request end to end.

Keeping CRM data safe and scoped

A few practical guardrails that matter more than model choice:

Managing access across a sales team

Once the assistant works for one rep, the next problem is managing API access across the whole team without sharing a single raw key. SubToAPI issues separate sub_live_... keys per environment or team member, tracks usage per key, and groups billing under Solo, Team, or Scale plans — so you can see which rep or workflow is driving usage without building that tracking yourself. Start with a free trial at /signup, then check /pricing for per-seat rates once you're ready to roll it out to the full team.

questions

Does the assistant need direct database access, or can it work through the CRM's existing API? Either works. If your CRM has a REST API (HubSpot, Salesforce, Pipedrive), wrap it in tool functions directly. For internal CRMs, query your database through an internal service layer — avoid giving Claude's tool calls raw SQL access.

How do I stop the assistant from making destructive changes by accident? Only expose read and append-style tools (like log_note) for autonomous execution. Anything that updates or deletes existing records should require explicit user confirmation in the UI before the tool call runs.

Can this work with multiple sales reps using the same assistant simultaneously? Yes — keep conversation state per user session, scope every CRM tool call to that user's permissions, and use separate API keys per seat so usage and costs are attributable per rep rather than pooled.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →