All endpoints are prefixed with /api. Request and response bodies are application/json unless noted otherwise. The server runs on http://localhost:3210 by default.
Authentication: off by default. When AUTH_REQUIRED=true or API_TOKEN is configured, every /api and /uploads request needs either Authorization: Bearer <API_TOKEN> (scripts, MCP) or the session cookie set by POST /api/auth/login (the web app). See Configuration.
Rate limits: endpoints that trigger billable AI calls or outbound fetches are limited to 60 requests/minute per client IP (429 RATE_LIMITED).
Fetch all active (non-archived, non-ghost, non-merged) contacts.
Query Parameters:
| Param | Description |
|---|---|
q |
FTS5 full-text search query |
view |
Set to slim for lightweight response (id, name, company, avatarUrl, themeColor) |
# All contacts
curl http://localhost:3210/api/contacts
# FTS5 search
curl "http://localhost:3210/api/contacts?q=engineer"
# Slim view (for caches, pickers)
curl "http://localhost:3210/api/contacts?view=slim"const contacts = await fetch("/api/contacts?view=slim").then((r) => r.json());Fetch a single contact with all hydrated child arrays (emails, phones, tags, lists, education, experience, etc.).
curl http://localhost:3210/api/contacts/abc123Response shape:
{
"id": "abc123",
"name": "Jane Smith",
"company": "Acme Corp",
"role": "VP Engineering",
"emails": [
{ "id": "e1", "email": "jane@acme.com", "label": "work", "isPrimary": true }
],
"phones": [
{
"id": "p1",
"phone": "+14155551234",
"label": "mobile",
"isPrimary": true
}
],
"tags": [{ "id": "t1", "tag": "investor" }],
"lists": [{ "id": "l1", "name": "Board Members", "icon": "👥" }],
"interactionCount": 12,
"relationshipScore": 85,
"...": "all other fields"
}Create a new contact.
Request Body:
{
"name": "Jane Smith",
"company": "Acme Corp",
"role": "VP Engineering",
"location": "San Francisco, CA",
"emails": [{ "value": "jane@acme.com", "label": "work" }],
"phones": [{ "value": "+14155551234", "label": "mobile" }],
"tags": [{ "tag": "investor" }]
}curl -X POST http://localhost:3210/api/contacts \
-H "Content-Type: application/json" \
-d '{"name":"Jane Smith","company":"Acme Corp","role":"VP Engineering"}'const contact = await fetch("/api/contacts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Jane Smith",
company: "Acme Corp",
role: "VP Engineering",
}),
}).then((r) => r.json());Returns: 201 with the created contact object.
Full update with nested child arrays. Replaces child arrays entirely.
curl -X PUT http://localhost:3210/api/contacts/abc123 \
-H "Content-Type: application/json" \
-d '{"name":"Jane Smith-Johnson","emails":[{"value":"jane@newco.com","label":"work"}]}'Partial scalar update. Does not support child arrays — use PUT for those.
curl -X PATCH http://localhost:3210/api/contacts/abc123 \
-H "Content-Type: application/json" \
-d '{"company":"NewCo","role":"CTO"}'Cascade delete including vec0 embeddings, FTS5 entries, and interaction mentions.
curl -X DELETE http://localhost:3210/api/contacts/abc123Bulk create contacts from an import. Supports SSE streaming for multi-phase import progress.
Request Body:
{
"contacts": [
{ "name": "Alice Johnson", "company": "TechCorp" },
{ "name": "Bob Williams", "role": "Designer" }
]
}Standard mode:
curl -X POST http://localhost:3210/api/contacts/bulk \
-H "Content-Type: application/json" \
-d '{"contacts":[{"name":"Alice Johnson"},{"name":"Bob Williams"}]}'SSE streaming mode (for progress tracking):
curl -X POST http://localhost:3210/api/contacts/bulk \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"contacts":[...]}'The SSE stream sends progress events through 4 phases:
importing— Contact creation progressembedding— Generating contact fingerprintsscanning— Looking for duplicatesdone— Summary with counts (imported, auto-merged, needs-review, new-unique)
Bulk delete by ID array.
curl -X POST http://localhost:3210/api/contacts/bulk-delete \
-H "Content-Type: application/json" \
-d '{"ids":["abc123","def456"]}'Bulk update shared fields across multiple contacts.
curl -X PUT http://localhost:3210/api/contacts/bulk-update \
-H "Content-Type: application/json" \
-d '{"ids":["abc123","def456"],"data":{"company":"NewCo"}}'AI-parse unstructured text into a structured contact record.
curl -X POST http://localhost:3210/api/parse-contact \
-H "Content-Type: application/json" \
-d '{"text":"Met Jane Smith at the TechCrunch event. She is VP of Engineering at Acme Corp. jane@acme.com, (415) 555-1234."}'Response:
{
"name": "Jane Smith",
"role": "VP of Engineering",
"company": "Acme Corp",
"emails": [{ "value": "jane@acme.com", "label": "work" }],
"phones": [{ "value": "+14155551234", "label": "work" }]
}Fetch all archived contacts.
curl http://localhost:3210/api/contacts/archivedFetch geocoded contacts for the map view (only those with lat/lng coordinates).
curl http://localhost:3210/api/contacts/mapUpload an avatar image. Uses multipart/form-data.
curl -X POST http://localhost:3210/api/contacts/abc123/avatar \
-F "avatar=@photo.jpg"Single-contact enrichment via AI web grounding. Uses the provider-appropriate strategy (two-pass for Gemini, single-pass for OpenAI/Anthropic).
curl -X POST http://localhost:3210/api/contacts/abc123/enrichResponse:
{
"success": true,
"fieldsUpdated": 5,
"latencyMs": 2340,
"models": ["gemini-2.5-flash"],
"tokenCount": 1250
}Error codes: 429 (grounding quota exhausted), 503 (AI not configured).
Fetch chronological timeline with @mention links.
curl http://localhost:3210/api/contacts/abc123/timelineLog a new interaction. Triggers async @mention extraction.
Request Body:
{
"type": "note",
"title": "Coffee meeting",
"content": "Discussed the Series B with @John Doe. Great progress on the product roadmap.",
"date": "2025-01-15T10:00:00Z"
}Supported types: note, call, meeting, email, message, sms.
Optional actionItem field to create a linked follow-up:
{
"type": "call",
"title": "Quarterly check-in",
"content": "Need to follow up on proposal.",
"date": "2025-01-15T10:00:00Z",
"actionItem": {
"title": "Send proposal draft",
"dueAt": "2025-01-22T00:00:00Z"
}
}curl -X POST http://localhost:3210/api/contacts/abc123/interactions \
-H "Content-Type: application/json" \
-d '{"type":"note","title":"Meeting notes","content":"Great discussion.","date":"2025-01-15T10:00:00Z"}'Edit an existing interaction.
curl -X PATCH http://localhost:3210/api/interactions/int123 \
-H "Content-Type: application/json" \
-d '{"content":"Updated meeting notes with corrections."}'Remove an interaction.
curl -X DELETE http://localhost:3210/api/interactions/int123Generate an AI "Catch-Me-Up" briefing from the contact's timeline history.
curl -X POST http://localhost:3210/api/contacts/abc123/briefingResponse:
{
"briefing": "**Wins:** Closed Series B at $12M valuation...\n**Projects:** Building out the platform team...\n**Open Loops:** Waiting on legal review for partnership agreement..."
}Promote a Ghost contact to a full contact.
curl -X POST http://localhost:3210/api/contacts/ghost123/promoteUpload a file attachment to a contact. Uses multipart/form-data.
curl -X POST http://localhost:3210/api/contacts/abc123/attachments \
-F "attachment=@document.pdf"FTS5 keyword search (used by the sidebar search bar).
curl "http://localhost:3210/api/search?q=engineer+san+francisco"Hybrid semantic search (Ask Contrack v3). Supports NDJSON streaming for progressive results.
Request Body:
{
"query": "who works in fintech and I haven't talked to recently"
}Standard JSON response:
curl -X POST http://localhost:3210/api/search/semantic \
-H "Content-Type: application/json" \
-d '{"query":"fintech contacts in San Francisco"}'NDJSON streaming (two-phase progressive results):
curl -X POST http://localhost:3210/api/search/semantic \
-H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-d '{"query":"fintech contacts"}'Phase 1 (instant retrieval <15ms): returns matches with aiReason: null.
Phase 2 (~500ms later): returns matches with AI-generated reasons.
Synthesize search results into an executive brief. Streams via NDJSON.
curl -X POST http://localhost:3210/api/search/synthesize \
-H "Content-Type: application/json" \
-H "Accept: application/x-ndjson" \
-d '{"query":"fintech contacts","contactIds":["abc123","def456"]}'Start a batch enrichment job for selected contacts.
Request Body:
{
"contactIds": ["abc123", "def456", "ghi789"],
"strategy": "two-pass"
}Strategy defaults to the provider-appropriate strategy if omitted.
curl -X POST http://localhost:3210/api/ai-search \
-H "Content-Type: application/json" \
-d '{"contactIds":["abc123","def456"]}'Response:
{
"batchId": "batch-abc123",
"jobCount": 2
}Poll the current status of a batch enrichment job.
curl "http://localhost:3210/api/ai-search/status?batchId=batch-abc123"Subscribe to real-time batch progress via Server-Sent Events (SSE).
curl -N "http://localhost:3210/api/ai-search/stream?batchId=batch-abc123"const eventSource = new EventSource(`/api/ai-search/stream?batchId=${batchId}`);
eventSource.onmessage = (event) => {
const batch = JSON.parse(event.data);
console.log(`Status: ${batch.status}, Jobs: ${batch.jobs.length}`);
if (batch.status === "complete") eventSource.close();
};Trigger a full deduplication scan. Streams progress via SSE.
Request Body:
{
"mode": "full"
}Supported modes: deterministic, ai, both, quick, deep, full.
curl -X POST http://localhost:3210/api/dedupe/scan \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"mode":"full"}'Fetch pending dedupe clusters awaiting review.
curl http://localhost:3210/api/dedupe/suggestionsGet the count of pending suggestions (for badges).
curl http://localhost:3210/api/dedupe/suggestions/countMerge a suggestion cluster.
Request Body:
{
"primaryId": "abc123"
}curl -X POST http://localhost:3210/api/dedupe/suggestions/sug123/merge \
-H "Content-Type: application/json" \
-d '{"primaryId":"abc123"}'Dismiss a suggestion (adds to exclusion list — won't be suggested again).
curl -X POST http://localhost:3210/api/dedupe/suggestions/sug123/dismissManual 2-contact merge.
curl -X POST http://localhost:3210/api/contacts/merge \
-H "Content-Type: application/json" \
-d '{"primaryId":"abc123","duplicateId":"def456"}'Merge an N-contact cluster.
curl -X POST http://localhost:3210/api/contacts/merge-cluster \
-H "Content-Type: application/json" \
-d '{"primaryId":"abc123","duplicateIds":["def456","ghi789"]}'Fetch the audit trail of past merges.
curl http://localhost:3210/api/dedupe/merge-logUndo a previous merge.
curl -X POST http://localhost:3210/api/dedupe/merge-log/ml123/undoFetch all pending (incomplete) action items.
curl http://localhost:3210/api/action-itemsFetch completed action items.
curl http://localhost:3210/api/action-items/completedGet count of urgent (due/overdue) action items (for badges).
curl http://localhost:3210/api/action-items/countCreate an action item for a contact.
curl -X POST http://localhost:3210/api/contacts/abc123/action-items \
-H "Content-Type: application/json" \
-d '{"title":"Send follow-up email","dueAt":"2025-02-01T00:00:00Z"}'Fetch action items for a specific contact.
curl http://localhost:3210/api/contacts/abc123/action-itemsUpdate an action item.
curl -X PATCH http://localhost:3210/api/action-items/ai123 \
-H "Content-Type: application/json" \
-d '{"title":"Updated title","dueAt":"2025-02-15T00:00:00Z"}'Mark an action item as complete.
curl -X PATCH http://localhost:3210/api/action-items/ai123/completeDelete an action item.
curl -X DELETE http://localhost:3210/api/action-items/ai123Fetch Relationship Pulse Dashboard metrics.
curl http://localhost:3210/api/dashboardGet AI-generated daily insight about your network.
curl http://localhost:3210/api/dashboard/insightCRM intelligence signals for the Command Palette zero-state (action items due, at-risk contacts, ghosts, stale data, dedupe suggestions).
curl http://localhost:3210/api/command-palette/zero-stateResponse:
{
"insights": [
{ "type": "action_items", "label": "3 action items due today", "count": 3 },
{
"type": "at_risk",
"label": "Haven't contacted Sarah Chen in 45 days",
"contact": { "id": "...", "name": "Sarah Chen" },
"daysSince": 45
},
{
"type": "ghost",
"label": "John mentioned 5 times but not in contacts",
"contact": { "id": "...", "name": "John" },
"mentionCount": 5
}
]
}Fetch all lists with member counts.
curl http://localhost:3210/api/listsCreate a new list.
curl -X POST http://localhost:3210/api/lists \
-H "Content-Type: application/json" \
-d '{"name":"Board Members","icon":"👥"}'Update a list (name, icon).
curl -X PATCH http://localhost:3210/api/lists/list123 \
-H "Content-Type: application/json" \
-d '{"name":"Advisory Board","icon":"🎯"}'Delete a list (members are unlinked, not deleted).
curl -X DELETE http://localhost:3210/api/lists/list123Reorder lists via an ordered ID array.
curl -X PUT http://localhost:3210/api/lists/reorder \
-H "Content-Type: application/json" \
-d '{"orderedIds":["list2","list1","list3"]}'Fetch contacts in a specific list.
curl http://localhost:3210/api/lists/list123/contactsAdd a contact to a list.
curl -X POST http://localhost:3210/api/lists/list123/members \
-H "Content-Type: application/json" \
-d '{"contactId":"abc123"}'Remove a contact from a list.
curl -X DELETE http://localhost:3210/api/lists/list123/members/abc123Bulk add contacts to a list.
curl -X POST http://localhost:3210/api/lists/list123/members/bulk \
-H "Content-Type: application/json" \
-d '{"contactIds":["abc123","def456","ghi789"]}'Backs Settings → AI. Capabilities are quick, deep, embeddings, and
research. See Configuration for what each
one powers.
The full configuration view: connected providers (with redacted key previews), built-in providers not yet configured, custom endpoints, every capability's assignment plus what it currently resolves to, and the SearXNG URL.
curl http://localhost:3210/api/settings/ai{
"providers": [
{
"id": "gemini",
"label": "Google Gemini",
"kind": "gemini",
"source": "env",
"keyPreview": "••••YJWY",
"modelCount": 45,
"supportsDiscovery": true,
"supportsGrounding": true
}
],
"availableProviders": [{ "id": "openai", "label": "OpenAI" }],
"customEndpoints": [],
"capabilities": {
"quick": {
"assignment": { "mode": "auto" },
"resolved": { "providerId": "gemini" }
},
"embeddings": { "assignment": { "mode": "auto" }, "resolved": null }
},
"searxngUrl": null
}A raw API key is never returned — only keyPreview.
Models eligible for a capability, grouped by provider. Chat models for
quick/deep/research, embedding models for embeddings.
curl http://localhost:3210/api/settings/ai/models/deep{
"groups": [
{
"providerId": "gemini",
"providerLabel": "Google Gemini",
"models": [
{
"id": "gemini-3.6-flash",
"label": "Gemini 3.6 Flash",
"capabilities": ["chat"],
"capabilityConfidence": "declared"
}
]
}
]
}capabilityConfidence is declared when the provider reports what a model can
do (Gemini, Anthropic) and guessed when it was inferred from the model name
(OpenAI and OpenAI-compatible servers return bare ids).
Store an API key for a built-in provider (gemini, openai, anthropic) and
immediately validate it by discovering models. Returns 502 DISCOVERY_FAILED if
the provider rejects the key — the key is still stored so it can be corrected.
curl -X PUT http://localhost:3210/api/settings/ai/providers/anthropic/key \
-H "Content-Type: application/json" \
-d '{"apiKey":"sk-ant-..."}'{ "success": true, "modelCount": 11 }Keys set via environment variable take precedence and cannot be overwritten here.
Remove a stored key. Environment-provided keys are unaffected.
Re-query a provider's model list, bypassing the 24-hour cache.
{ "modelCount": 45, "fetchedAt": "2026-08-05T00:00:00.000Z" }Assign a capability. mode is auto, pinned, or disabled; pinned
requires providerId. Assigning embeddings triggers a background vector-store
rebuild if the model's dimension differs.
curl -X PUT http://localhost:3210/api/settings/ai/capabilities/deep \
-H "Content-Type: application/json" \
-d '{"mode":"pinned","providerId":"anthropic","model":"claude-sonnet-5"}'{ "success": true, "view": { "...": "the full settings view" } }Add or update a custom OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, llama.cpp, OpenRouter…). Validates connectivity by listing models; the endpoint is stored even when that fails, so an offline server can be configured ahead of time.
curl -X PUT http://localhost:3210/api/settings/ai/endpoints \
-H "Content-Type: application/json" \
-d '{"id":"homelab","label":"Homelab Ollama","baseUrl":"http://alpha:11434/v1"}'Remove a custom endpoint.
Set the SearXNG base URL for self-hosted web research. An empty string clears it.
curl -X PUT http://localhost:3210/api/settings/ai/searxng \
-H "Content-Type: application/json" \
-d '{"url":"http://searxng.local:8080"}'Get AI model routing and quota diagnostics.
curl http://localhost:3210/api/ai/diagnosticsCheck AI Search grounding RPD quota (Gemini only).
curl http://localhost:3210/api/ai/grounding-capacityGet aggregated AI usage statistics (token counts, cache performance, estimated costs).
curl http://localhost:3210/api/ai/stats/summaryGet the AI invocation feed (paginated).
Query Parameters:
| Param | Description |
|---|---|
limit |
Number of items to return (default: 50) |
offset |
Pagination offset |
curl "http://localhost:3210/api/ai/stats/feed?limit=20&offset=0"Extract OpenGraph metadata (title, image, description) from a URL using Cheerio HTML parsing. No headless browser required.
curl "http://localhost:3210/api/link-preview/unfurl?url=https://example.com"Fetch a company logo by domain. Proxies through Google S2 Favicons and caches locally for offline access.
curl http://localhost:3210/api/logos/stripe.comReturns the image binary with appropriate content-type headers.
Machine-readable query endpoints for programmatic access.
Query contacts with filter parameters.
curl "http://localhost:3210/api/query/contacts?q=engineer&limit=10"Fetch action items across all contacts.
curl http://localhost:3210/api/contacts/action-itemsGet all unique tags.
curl http://localhost:3210/api/tagsGet all unique industries.
curl http://localhost:3210/api/industriesSearch interactions by content.
curl "http://localhost:3210/api/interactions/search?q=proposal"Fetch the global timeline (all interactions across all contacts).
curl http://localhost:3210/api/timelineEvery endpoint under /api/auth is mounted before the auth gate, so it stays reachable to a caller with no credential. Endpoints marked (account) additionally require a signed-in session — an API_TOKEN bearer is not enough, because there is no account behind a shared token (403 USER_REQUIRED).
Always reachable. One round trip for everything the client needs to pick a screen.
curl http://localhost:3210/api/auth/status
# → { "authRequired": true, "authenticated": false, "setupRequired": true,
# "hasAccounts": false, "user": null }setupRequired is true only on a gated instance with no accounts.
Create the first account. Returns 409 SETUP_COMPLETE once any account exists, so this is not a standing registration endpoint. The new account is an admin, is signed in immediately, and claims every unowned row in the database.
curl -X POST http://localhost:3210/api/auth/setup \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","username":"you","password":"a long passphrase","displayName":"You"}'Rate limited to 5/minute per IP.
Exchange credentials for an HttpOnly SameSite=Strict session cookie. identifier accepts either the username or the email.
curl -X POST http://localhost:3210/api/auth/login \
-H "Content-Type: application/json" \
-d '{"identifier": "you", "password": "a long passphrase"}'Returns 401 INVALID_CREDENTIALS for both a wrong password and an unknown account — the two are deliberately indistinguishable. Rate limited to 10/minute per IP.
POST /api/auth/logout destroys the session server-side and clears the cookie.
The signed-in account. PATCH /api/auth/me updates displayName, username, or email; omitted fields are left alone.
curl -X POST http://localhost:3210/api/auth/change-password \
-H "Content-Type: application/json" -b cookies.txt \
-d '{"currentPassword": "old one", "newPassword": "a new long passphrase"}'Ends every session except the one making the request.
Live sessions for this account, newest first, with current: true on the one making the request. DELETE /api/auth/sessions revokes all the others and returns { "revoked": n }.
DELETE /api/contacts/:id and POST /api/contacts/bulk-delete are soft deletes — contacts move to the trash and are hard-deleted after TRASH_RETENTION_DAYS (default 30).
curl http://localhost:3210/api/trash
# → { "items": [{ "id", "name", "company", "avatarUrl", "deletedAt" }] }Restore a trashed contact to the active list (re-indexes search and embeddings). Returns the hydrated contact; 404 if the contact isn't in the trash.
"Delete forever" — immediately hard-deletes a trashed contact and its entire history. Refuses (404) for contacts that are not in the trash.
SQLite snapshots (online backup API — safe while the app runs) written to DATA_DIR/backups/, rotated to the BACKUP_KEEP most recent. A schedule runs every BACKUP_INTERVAL_HOURS (default 24).
curl http://localhost:3210/api/backups
# → { "backups": [{ "filename", "sizeBytes", "createdAt" }] }Take a snapshot now. Returns 201 with the new backup's metadata.
Downloads the entire database — contacts (hydrated), interactions, lists, action items, and the merge audit log — as a single JSON attachment.
Downloads a flat, RFC-4180-escaped CSV of all non-trashed contacts.
curl -OJ http://localhost:3210/api/export/csvExposes hit/miss counters for all aiCache tiers. Only available when NODE_ENV !== production.
curl http://localhost:3210/api/debug/cache-stats