MCP server
What your client must do to connect to Pairly's CRM MCP server.
Pairly runs a Model Context Protocol server for each business's CRM. An agent or MCP client connected to it can create and update contacts and companies, record account events and send transactional email, with the same effects as the signed webhook.
Requirements
A client that meets all of these can connect.
- Endpoint and transport. Streamable HTTP at
https://api.pairlyhq.com/v1/mcp/crm. The path works with or without a trailing slash. The server is stateless: every request stands alone, so do not depend on a session surviving between requests. - Authentication. Send
Authorization: Bearer <key secret>on every request. The secret is the same one used to sign webhooks (Keys). There is no OAuth flow. A401response names aresource_metadataURL in itsWWW-Authenticateheader, but nothing is served there, so ignore it. Every valid key carries the single scopecrm:write. - Business scoping. A key belongs to exactly one business, and the server derives the business from it. No tool takes a business id.
- Call from a server. Requests that carry an
Originheader are accepted only from Pairly's own API origins. Any other origin, includingapp.pairlyhq.com, gets403 Invalid Origin header. Browser-based clients are therefore not supported, and the secret must not reach a browser anyway. Server-side clients send noOriginand are unaffected. - Handle a revoked key. Rotating or revoking the key takes effect immediately and without notice. A missing, wrong or revoked key gets
401withWWW-Authenticate: Bearer error="invalid_token"and the JSON body{"error": "invalid_token", "error_description": "Authentication required"}. Treat it as "ask the business for a new key", not as something to retry. - Treat tool errors as data. A tool that fails returns a normal result with
isError: trueand one text item readingError executing tool <name>: <message>. Validation messages (for exampleemail is not a valid email address) mean the arguments must change.Could not record the event; please retryis a generic server-side failure that any tool can return. Retrying is safe for the upserts,upsert_companyanddelete_person.record_user_createdandrecord_email_confirmedadd a lifecycle note on every call, so a retry may add a duplicate note, and a retry ofrecord_eventmay log the event twice. Forsend_transactional_email, retry with the sameidempotency_key. A successful result is one text item holding JSON such as{"status": "ok", "person_id": "…"}.
Behaviour
- Contacts are matched by email. Upserts fill in empty fields and never overwrite a value someone already set.
- Tags are merged onto the contact. Sending fewer tags never removes one.
- On tools that take an
emailargument, an address that is invalid or longer than 320 characters is rejected before anything is written.send_transactional_emailvalidates itstoaddress separately. delete_personarchives the contact and is safe to repeat. A later upsert for the same email finds the archived contact and does not un-archive it.record_eventhas no idempotency key, unlike the webhook'suser.event. If you need exactly-once event logging with retries, use the webhook.send_transactional_emailis transactional only (receipts, password resets) and is sent once peridempotency_key. When the business has reached its sending limit the call fails with an error that says how long to wait; retry with the sameidempotency_key.
Full argument lists: tool reference.
Connecting
claude mcp add --transport http pairly-crm https://api.pairlyhq.com/v1/mcp/crm \
--header "Authorization: Bearer $PAIRLY_KEY_SECRET"
// ES module (top-level await): a .mts file, or "type": "module" in package.json
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const transport = new StreamableHTTPClientTransport(new URL('https://api.pairlyhq.com/v1/mcp/crm'), {
requestInit: { headers: { Authorization: `Bearer ${process.env.PAIRLY_KEY_SECRET}` } },
})
const client = new Client({ name: 'acme-site', version: '1.0.0' })
await client.connect(transport)
const result = await client.callTool({
name: 'upsert_person',
arguments: { email: 'jules@example.com', first_name: 'Jules', tags: ['website_lead'] },
})
import asyncio, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
headers = {"Authorization": f"Bearer {os.environ['PAIRLY_KEY_SECRET']}"}
async with streamablehttp_client("https://api.pairlyhq.com/v1/mcp/crm", headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("upsert_person", {"email": "jules@example.com", "tags": ["website_lead"]})
print(result.content)
asyncio.run(main())
Conformance checklist
Run these with MCP Inspector (npx @modelcontextprotocol/inspector): transport "Streamable HTTP", your URL, and the Authorization header.
-
initializesucceeds. -
tools/listreturns exactly the seven tools in the reference. -
upsert_personwith a test email returns{"status": "ok", "person_id": "…"}, and the contact appears in the business's CRM. - The same call with a wrong or revoked key is refused with HTTP
401.
You can also run the Python or TypeScript snippet above to perform these checks.