CRM & Contact Intelligence
Paid tier required
The CRM API and contact auto-extraction are available on Team and Enterprise plans only. Requests from a Starter team return HTTP 403.
Overview
xbrain maintains a live contact directory for each team, populated automatically from two sources: meeting notes ingested via the Granola integration and any memory item whose content mentions people. No manual data entry required.
Contacts share the same tagging contract as memory items — every record carries
team_scope, visibility, confidence,
truth_level, and validation_status. This means
contact data participates in the same truth-level promotion and scoped retrieval
pipeline as the rest of the team's knowledge.
Automatic Contact Extraction
Every call to POST /v1/memory/upsert triggers a background hook,
_extract_crm_contacts, that uses Claude to identify people mentioned
in the memory content. Extracted contacts are upserted on the unique key
(team_scope, email) — if a contact already exists, only new fields
are added; existing data is never overwritten.
Fail-soft by design
If Claude extraction fails or returns no contacts, the memory upsert still succeeds. The background hook is fire-and-forget. Contact extraction never blocks the write path.
The same extraction runs on Granola meeting notes during ingest — participants
from the participants field are directly upserted as contacts without
requiring LLM extraction.
Contact Fields
| Field | Type | Description |
|---|---|---|
id |
UUID | Auto-generated primary key. |
team_scope |
string | Team this contact belongs to. Hard isolation — Team A cannot see Team B's contacts. |
email |
string | Unique within a team. The deduplication key for upserts. |
name |
string | null | Display name extracted from meeting notes or memory content. |
company |
string | null | Company or organisation. |
role |
string | null | Job title or role. |
notes |
string | null | Free-form notes about the contact. |
source |
string | Origin: granola, manual, or a memory item source string. |
visibility |
enum | team (default), project, or private. |
confidence |
float | 0.0–1.0. Set by extraction pipeline; higher for direct meeting participants. |
truth_level |
enum | Epistemic status. Defaults to WORKING for extracted contacts. |
validation_status |
enum | pending, approved, or rejected. |
API Reference
All endpoints require a valid JWT and the X-Team-Scope header.
Team and Enterprise plans only — Starter teams receive 403.
List contacts
bashGET /v1/crm/contacts
curl "https://api.grooveos.app/v1/crm/contacts?limit=50&offset=0" \
-H "Authorization: Bearer $JWT" \
-H "X-Team-Scope: excalibur"
# Returns paginated list of contacts for the team
Create a contact
bashPOST /v1/crm/contacts
curl -X POST "https://api.grooveos.app/v1/crm/contacts" \
-H "Authorization: Bearer $JWT" \
-H "X-Team-Scope: excalibur" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"name": "Alice Martin",
"company": "Acme Corp",
"role": "CTO",
"notes": "Met at SaaS Summit 2026",
"visibility": "team",
"confidence": 1.0,
"truth_level": "WORKING",
"validation_status": "pending"
}'
Upsert a contact
Use PUT to create-or-update a contact by (team_scope, email).
Existing fields are overwritten only if the new value is non-null.
bashPUT /v1/crm/contacts
curl -X PUT "https://api.grooveos.app/v1/crm/contacts" \
-H "Authorization: Bearer $JWT" \
-H "X-Team-Scope: excalibur" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"company": "New Corp"
}'
Update a contact
bashPATCH /v1/crm/contacts/{contact_id}
curl -X PATCH "https://api.grooveos.app/v1/crm/contacts/c1a2b3c4..." \
-H "Authorization: Bearer $JWT" \
-H "X-Team-Scope: excalibur" \
-H "Content-Type: application/json" \
-d '{"role": "VP Engineering", "truth_level": "VALIDATED"}'
Delete a contact
bashDELETE /v1/crm/contacts/{contact_id}
curl -X DELETE "https://api.grooveos.app/v1/crm/contacts/c1a2b3c4..." \
-H "Authorization: Bearer $JWT" \
-H "X-Team-Scope: excalibur"
Audit Log
Every mutation (create, update, upsert, delete) writes an entry to the
audit_log table with action, actor_id,
team_scope, and a JSON diff of what changed.
Query the audit trail via GET /v1/audit?resource=contact.
Error Reference
| HTTP Status | When | Resolution |
|---|---|---|
403 |
Team is on the Starter plan | Upgrade to Team or Enterprise to access the CRM API. |
409 |
Email already exists in this team (on POST, not PUT) | Use PUT /v1/crm/contacts to upsert instead of POST. |
422 |
Missing required fields (email) | Email is the only required field on creation. |
404 |
Contact ID not found or belongs to another team | Verify the contact ID and X-Team-Scope header. |