Skip to content
Collective StarsLet’s talk

Build

Giving a client AI access to their CRM without breaking your white label

Cornel Isac8 min read

How do I give a client AI access to their CRM data without breaking my white label?

Give clients AI access through a connector you control, with credentials held on your server and permissions tied to their CRM account. To preserve your white label, control the sign-in flow, tool names, results and errors too. A custom domain alone cannot hide the platform details a proxy passes through.

The client wants their pipeline in the tool they already use

Your client wants their AI assistant to read the pipeline. You want to say yes without sending them to the CRM platform behind your logo.

Your domain is on the login page. Your name is on the invoice. The client knows where to go when something breaks. Then a connector setup asks for a different company's URL, and the product you have carefully presented as a single service starts showing its seams.

MCP, the Model Context Protocol, lets an AI application discover and call tools on a server. HighLevel's own setup guide provides a Claude-specific endpoint at https://services.leadconnectorhq.com/mcp/anthropic/v2. It is a supported route to the client's data. It also puts an upstream hostname directly into the connection.

You can put a server on your own domain in front of it. The interesting question is what that server actually changes.

We traced a proxy that looks up the client's account, checks its permitted tools and attaches the platform credential before forwarding the request. Useful work. But it keeps the upstream tool names and relays the responses. The credential stays behind the counter; the platform's vocabulary can still walk out through the front door.

That is the gap to close if the connector is going to feel like part of your service.

The token shortcut moves the responsibility onto the client

Handing over a private integration token gets you a connection, but it also gives the client a credential to store, protect and replace. A bearer credential grants its holder the permissions attached to it.

Those permissions matter. HighLevel's scope reference separates contacts.readonly, contacts.write and conversations/message.write. Reading contacts does not inherently authorise sending messages. OAuth, the delegated sign-in flow, and private tokens both have scope controls; the available scopes can differ between them.

So the argument for your own connector is not that the platform forgot permissions. It is that you want to manage the client-facing connection and decide which permitted actions your service offers.

The client might need a pipeline lookup without a bulk export. A write might need a review step. You might need to revoke a staff member's connection without replacing the platform credential used by the rest of the integration. Those are policies you can put in your own layer.

The upstream scopes still set the ceiling. Your connector can offer less authority, never more.

That gives you a practical choice: use the official connection when its presentation and permissions fit, or take responsibility for a smaller interface that fits the service you sell. The responsibility does not disappear because the setup screen gets shorter.

The parts your own domain does not cover

A branded connector needs control over the conversation as well as the address. A facade is the layer that presents your own interface over the upstream service.

The proxy we checked already handles credential injection and rejects named tool calls outside the client's tier before decrypting the credential. It filters tool discovery when the platform returns ordinary JSON. It forwards streamed discovery unchanged.

That distinction is visible to a client. Server-sent events, or SSE, deliver a stream of events rather than a single JSON response. A filter that only understands JSON leaves another route for upstream tool names to appear. Blocking a call can protect an action while the tool list still advertises it.

These are the surfaces a facade has to own:

SurfaceWhat the client should encounter
Connection and sign-inYour service name and an authentication flow you control
Tool discoveryThe tools you offer, named and described in your vocabulary
Tool executionA permission check even if the assistant already knows the tool name
ResultsThe fields needed for the task, with upstream metadata deliberately mapped
ErrorsA useful explanation without raw upstream messages or URLs

The source demonstrates the start of that work. The raw relay explains why the white label is unfinished. Renaming the server does not rename a tool, and replacing the error page does not change the error text inside a successful HTTP response.

Resolve the account before reaching for a credential

Every tool call needs an account boundary the assistant cannot choose for itself. In a shared service, a tenant is the client account whose data and permissions must stay separate from the others.

The order matters: authenticate the connection, check the tool, validate the arguments, then obtain the upstream credential. An unrecognised or disallowed tool should stop before a credential is fetched.

Here is a transport-independent example of that request boundary. The adapters are explicit: authenticate checks expiry and revocation, parse accepts only that tool's documented arguments and rejects account selectors, run binds the server-selected account, and present returns the fields your connector exposes. The inspected relay would need those adapters to become a facade.

type Tenant = {
  id: string;
  accountId: string;
  allowedTools: ReadonlySet<string>;
};

type Tool = {
  parse(args: unknown): Record<string, unknown>;
  run(
    args: Record<string, unknown>,
    context: { accountId: string; token: string },
  ): Promise<unknown>;
  present(result: unknown): unknown;
};

type Dependencies = {
  authenticate(token: string): Promise<Tenant | undefined>;
  credentialFor(tenantId: string, accountId: string): Promise<string>;
  tools: ReadonlyMap<string, Tool>;
};

export async function callTool(
  authorization: string | null,
  request: { name: unknown; arguments: unknown },
  deps: Dependencies,
): Promise<unknown> {
  const bearer = authorization?.match(/^Bearer (\S+)$/i)?.[1];
  if (!bearer) throw new Error('Connection required');

  let tenant: Tenant | undefined;
  try {
    tenant = await deps.authenticate(bearer);
  } catch {
    throw new Error('Connection unavailable');
  }
  if (!tenant) throw new Error('Connection unavailable');

  const name = request.name;
  if (typeof name !== 'string' || !tenant.allowedTools.has(name)) {
    throw new Error('Tool unavailable for this connection');
  }
  const tool = deps.tools.get(name);
  if (!tool) throw new Error('Tool unavailable for this connection');

  let args: Record<string, unknown>;
  try {
    args = tool.parse(request.arguments);
  } catch {
    throw new Error('Invalid tool arguments');
  }

  try {
    const token = await deps.credentialFor(tenant.id, tenant.accountId);
    const result = await tool.run(args, { accountId: tenant.accountId, token });
    return tool.present(result);
  } catch {
    throw new Error('The CRM request could not be completed');
  }
}

The account context comes from the authenticated connection, separately from the assistant's arguments. Each adapter must preserve that separation. If a tool accepts a record identifier, test that the record belongs to the selected account; a header is not a substitute for that check. HighLevel's MCP documentation describes each operation as acting on a single sub-account, even when a connection authorises several.

Keep the connector's bearer credential out of URLs and logs. Log a non-secret account identifier, the tool, the outcome and a correlation ID instead. That gives you something to investigate without turning the investigation trail into another credential store.

Test the refusal, not just the successful lookup

A working lookup proves that a request can get through. The revealing test is whether the requests outside the agreement stop where they should.

The client may have cached a tool list before you changed their tier. Their assistant may send a missing name, an unexpected argument or a record identifier from the wrong account. Your server has to enforce the current policy on the call itself. Discovery is an invitation, not an authorisation check.

A release check should cover the awkward paths:

  • Revoke a connection, then try it again.
  • Call a disallowed tool directly rather than selecting it from discovery.
  • Send a missing tool name and a conflicting account argument.
  • Request a record belonging to a different account.
  • Inspect tool lists returned as both JSON and SSE.
  • Trigger an upstream failure and read the client response and your logs.

Check successful results for unwanted upstream fields too. An error mapper can be perfect while an ordinary response still carries an internal URL.

Do this with the permissions you actually intend to sell. Testing with an all-powerful credential can hide the missing scope that will break a client's first request. Testing only with a read-only credential says nothing about the review step you promised before a write. The service is the set of actions you permit and the boundaries you enforce around them.

What you now own

Your connector becomes a product surface you have to maintain. The bill includes authentication, revocation, tool mapping, monitoring and the support conversation when the platform is unavailable.

When an upstream field changes, you decide whether to map it, expose it or leave it out. When a tool disappears, the client needs a useful response. When a credential is rotated, any cached copy must stop being used. A credential cache must distinguish the tenant, account and credential version, with revocation tied to the relevant entry.

The tool list is therefore a product decision. Expose the work your clients actually need. Mirroring the entire platform gives you a much larger interface to explain, secure and keep current. A smaller set can be tested against the promises in your service, instead of inheriting whatever appeared upstream this week.

The records behind those tools need checking too. The data readiness review shows how a field can exist without supplying the signal a workflow needs. Permission to read the pipeline and a pipeline worth reading are separate pieces of work.

You also own the evidence. If a client asks why a record changed, an audit trail needs to connect the request, the authorised account and the result. Decide how you handle retries before offering writes, so a lost response does not turn into a repeated action.

Use the official connector when the client knows the platform and the available controls fit. Build the facade when your service needs a different experience or tighter policy. Then price the ongoing ownership into it. Your logo on the connector is the easy part; answering for what it does is the product.