Skip to content
MeridFlow AiFlow v8.x • self-hosted

CRM connectors

Connects an agent to Salesforce or HubSpot via OAuth2, then gives it three tools that act on that connection mid-conversation: crm_upsert_contact, crm_create_ticket, and crm_log_call_activity (see Tools). AiFlow holds the OAuth tokens, encrypted at rest, and refreshes them automatically. The underlying tool-calling mechanism doesn't change: a CRM action is just another function call the agent can make, the same as send_webhook or a custom tool.

Activating a provider

Each provider needs its own OAuth2 client credentials, set once for the whole deployment (not per agent, and not a customer credential) as backend environment variables. A provider with no client ID or secret set simply can't start a new connection: POST .../crm-connections/{id}/authorize-url still returns a URL, but the provider rejects it at that point.

Every provider's callback URL follows the same pattern:

{PUBLIC_BASE_URL}/api/v1/agents/{agent_id}/crm-connections/{provider}/oauth-callback

provider is the literal string salesforce or hubspot, and agent_id is the numeric id of the agent you're connecting (visible in that agent's URL in the admin dashboard). Both are known before a connection exists, so register the exact URL for your agent up front, for example https://api.your-domain.com/api/v1/agents/1/crm-connections/hubspot/oauth-callback. Every provider below also accepts more than one callback URL per app, in case you want to pre-register more than one agent id at once.

These provider consoles change without much notice

Salesforce and HubSpot both moved to a substantially different app-creation flow between early and mid-2026. The steps below reflect each provider's current flow as of this writing, with a link to the official doc at the top of each tab as the source of truth if a step stops matching what you see.

Official doc: Create an External Client App.

Salesforce restricted creating new Connected Apps starting Spring '26, in favor of External Client Apps, a newer container for the same OAuth mechanics. If your org still shows New Connected App instead of New External Client App, that's fine too: just follow the equivalent fields.

  1. Log into your Salesforce org (a Developer Edition org works fine for testing, no paid license needed).
  2. Setup (gear icon, top right) → search App in the Quick Find box → App Manager → New External Client App.
  3. Fill in the basic fields: Name (e.g. "AiFlow"), API Name (auto-derived, editable), Contact Email, and leave Distribution State as Local.
  4. Expand API (Enable OAuth Settings):
    • Check Enable OAuth.
    • Callback URL: your callback URL from above (this field accepts multiple URLs, one per line, if you want to pre-register a few ids).
    • Selected OAuth Scopes: add both Manage user data via APIs (api) and Perform requests at any time (refresh_token, offline_access). AiFlow only ever requests api refresh_token.
    • Under Security: deselect Require Proof Key for Code Exchange (PKCE) (AiFlow doesn't implement PKCE), and keep Require Secret for the Web Server Flow and Require Secret for Refresh Token Flow selected.
  5. Save. Salesforce shows a warning that changes can take up to 10 minutes to propagate; that's normal.
  6. To retrieve credentials: from App Manager, find your app → open its Settings tab → expand OAuth Settings → click Consumer Key and Secret (opens a new tab, may ask you to verify your identity via email first). This reveals:
    • Consumer Key → SALESFORCE_CLIENT_ID
    • Consumer Secret → SALESFORCE_CLIENT_SECRET
  7. If users besides System Administrators need to authorize this app themselves, look for a Policies or OAuth Policies section on the app (the External Client App equivalent of a Connected App's "Permitted Users"), and confirm it allows the users who'll actually click through the "Connect a CRM" flow later.

Official doc: Create a new app using the CLI.

This one changed the most. HubSpot retired the old "sign into developers.hubspot.com and click Create app" web flow for new OAuth apps (that button still works only for accounts that made an app before the mid-2026 cutover). A new OAuth app is created through the HubSpot CLI instead. It's still a one-time setup step, not something you need to maintain afterward.

  1. Install Node.js if you don't already have it, then install the CLI:
    npm install -g @hubspot/cli@latest
    
  2. Authenticate it with your HubSpot account (opens a browser to sign in and grant access):
    hs account auth
    
  3. Create a new project:
    hs project create
    
    When prompted: Contents → App; Distribution → restrict to specific accounts, not the Marketplace, since this is a private integration; Authentication → OAuth; Features → none needed, so skip or leave it blank.
  4. This scaffolds a project containing an app-hsmeta.json file. Open it and set its auth section:
    "auth": {
      "type": "oauth",
      "redirectUrls": ["your callback URL from above"],
      "requiredScopes": ["oauth", "crm.objects.contacts.write", "tickets"]
    }
    
    (oauth is a base scope every app needs, and it's easy to miss. hs project upload warns and auto-adds it if left out, but declare it explicitly rather than rely on that. Tickets aren't granular like contacts: there's no crm.objects.tickets.write, just the single tickets scope covering both read and write, and an unrecognized scope name here fails the whole upload with The scope ... could not be recognized. crm.objects.contacts.write alone already covers the contact search AiFlow does before deciding to create or update one, and the call-logging tool, since HubSpot's engagement APIs ride on the contacts scope rather than having their own).
  5. Upload it:
    hs project upload
    
  6. Open the project in your browser, find the credentials:
    hs project open
    
    Under Project Components, click your app's name → Auth tab → Client credentials section shows:
    • Client ID → HUBSPOT_CLIENT_ID
    • Client secret → HUBSPOT_CLIENT_SECRET
  7. If your HubSpot account is part of an agency or parent account, install the app on the specific sub-account (portal) you want AiFlow's connection to act on. The OAuth consent screen asks which portal to connect during the actual "Connect a CRM" step in AiFlow, not here.

Exact CLI prompt wording can shift between CLI releases; run hs project create --help if step 3's prompts don't match what you see.

Endpoint family

GET    /api/v1/agents/{agent_id}/crm-connections
POST   /api/v1/agents/{agent_id}/crm-connections
DELETE /api/v1/agents/{agent_id}/crm-connections/{connection_id}
GET    /api/v1/agents/{agent_id}/crm-connections/{connection_id}/authorize-url

Plus GET .../crm-connections/{provider}/oauth-callback, the OAuth redirect target itself. You don't call this directly; the provider's own authorize page redirects a browser there after the admin approves the connection. It's keyed by provider, not connection_id, so it's predictable before a connection row exists. See the callback URL note above.

Authentication and roles

Every endpoint here (except the callback, see below) takes an admin session token (see Authentication). GET is available to every role, including Viewer; POST and DELETE require Owner or Admin, the same bar as MCP servers and other credential-bearing connections.

The callback endpoint takes no admin token at all: the provider's redirect lands a browser there directly, with no Authorization header attached. Its state query parameter, matched against a nonce generated at authorize-url time, is the security boundary instead, the same model virtually every OAuth integration uses for this exact reason.

Connect a provider

POST /api/v1/agents/{agent_id}/crm-connections
Field Type Required Description
provider string Yes salesforce or hubspot.

An agent can have at most one connection per provider (409 on a second attempt). The response's instance_url field is populated automatically for Salesforce from the OAuth exchange; HubSpot's API always lives at one fixed host, so it stays null there. It's never admin-supplied.

curl -X POST https://api.your-domain.com/api/v1/agents/1/crm-connections \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{"provider": "hubspot"}'
{
  "id": 3,
  "agent_id": 1,
  "provider": "hubspot",
  "status": "pending",
  "instance_url": null,
  "last_error": null,
  "created_at": "2026-01-15T10:00:00Z"
}

Then fetch the authorize URL and open it in a browser to complete the OAuth flow:

GET /api/v1/agents/{agent_id}/crm-connections/{connection_id}/authorize-url
{ "authorize_url": "https://app.hubspot.com/oauth/authorize?..." }

The provider redirects back to AiFlow's own callback once the admin approves. A small static page confirms success or reports what went wrong (an expired authorization code, a mismatched redirect URI, and so on), then GET .../crm-connections reflects the connection's new status (connected or error, with last_error set on failure).

The tools

crm_upsert_contact, crm_create_ticket, and crm_log_call_activity (see Tools) all share one config field, provider, shown as a dropdown of salesforce or hubspot in the admin dashboard's Tools tab. Whichever provider you pick determines which of this agent's connections the tool uses when it's called. Enabling one of these tools without a matching connection returns a plain error string to the agent rather than failing the call, the same defensive pattern every built-in tool follows.

Next

  • Tools: the general tool-configuration model these three tools share with every other built-in and custom tool.
  • Calls: crm_log_call_activity is typically called near the end of a call, alongside record_summary.