CRM Integration
SM CRM (api.sprintmode.ai) is the single source of truth for contacts, companies, deals, activities, and engagements. All SM products read and write to the same DB — tagged by product field to keep data scoped.
The Product Tag
Tag every record with product to scope it to your product:
# Create a company tagged to Mode
curl -X POST https://api.sprintmode.ai/api/companies \
-H "X-SM-Key: $SM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"product": "mode",
"company_type": "client"
}'Then filter by your product when reading:
GET /api/companies?product=mode
GET /api/contacts?product=mode
GET /api/engagements?product=modeKey Relationships
companies (Acme Corp)
└── contacts (Jane Smith — portal_access: 1)
└── engagements (Mode implementation retainer)
└── invoices (INV-2026-001)
└── deals (Mode expansion deal)
contacts
└── activities (call, email, note, transcript)
└── magic_sessions (login tokens)Granting Portal Access
# Grant Jane portal access to your product
curl -X PATCH https://api.sprintmode.ai/api/contacts/ct_abc123 \
-H "X-SM-Key: $SM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"portal_access": 1,
"product": "mode",
"portal_role": "client_admin"
}'Jane can now:
- Click a magic link → log in to
mode.sprintmode.ai - Log in with Google SSO (if her email is in CRM)
Her JWT will include "products": ["mode"].
Logging Activities
Every significant action should generate an activity:
// From a product worker or page action
await api('/api/activities', null, {
method: 'POST',
body: {
activity_type: 'scan_completed',
company_id: 'co_abc123',
contact_id: 'ct_def456',
title: 'Mode scan completed',
description: 'Full technology audit finished. 47 systems mapped.',
source: 'mode',
metadata: { scan_id: 'scan_xyz', systems_found: 47 }
}
})Activity types are free-form — use descriptive names like scan_completed, report_generated, phase_started.
Calling CRM from a Portal Worker
Portal workers call SM API server-to-server using CF Access service token credentials:
// In _worker.js
async function smApi(path, env, options = {}) {
const res = await fetch('https://api.sprintmode.ai' + path, {
method: options.method || 'GET',
headers: {
'Content-Type': 'application/json',
'CF-Access-Client-Id': env.SM_API_CLIENT_ID,
'CF-Access-Client-Secret': env.SM_API_CLIENT_SECRET,
...(options.headers || {})
},
body: options.body ? JSON.stringify(options.body) : undefined
})
if (!res.ok) throw new Error('SM API error: ' + res.status)
return res.json()
}
// Usage
const { data: company } = await smApi('/api/companies/co_abc123', env)
const { data: engagements } = await smApi('/api/engagements?company_id=co_abc123&product=mode', env)Calling CRM from the Browser
Use the api() helper which sends cookies automatically:
import { api } from '@nomadahq/sm-ui'
// GET with query params
const { data: contacts } = await api('/api/contacts', {
company_id: 'co_abc123',
product: 'mode'
})
// POST
await api('/api/activities', null, {
method: 'POST',
body: { activity_type: 'report_viewed', company_id: 'co_abc123' }
})
// PATCH
await api('/api/contacts/ct_abc123', null, {
method: 'PATCH',
body: { pipeline_stage: 'onboarding' }
})Using the MCP Server
If you're building an AI agent that needs to read/write CRM data, use the SM MCP server instead of calling the API directly:
SSE endpoint: https://api.sprintmode.ai/mcp/sse
Auth: Bearer <SM_API_KEY>The MCP server exposes all CRM data as tools that Claude can call. Register it in your Claude project settings.
Data Isolation
All SM products share the same CRM DB. Data isolation is by convention (the product tag), not by database separation. This means:
- Any SM API key can read any product's data
- Admin portal users can see all products' data
- Use the
productfilter consistently — don't skip it
For products that need stricter isolation (e.g. competitor-sensitive client data), contact Aaron to discuss row-level security options.
