Last Updated: 2026-06-29 Status: Active
1. Service Overview
| Property | Value |
|---|---|
| Service Owner | apps/contact-intelligence |
| Base URL | https://api.logicspike.com/contact-intel (via Gateway) |
| Internal Microservice | contact-intelligence.logicspike.workers.dev |
| Gateway Key Header | x-gateway-key |
| Primary Consumer | Chat Engine (service-to-service) |
| Secondary Consumer | AI Brain (via brain tools) |
2. Authentication Requirements
| Domain | Auth Type | Header | Consumer |
|---|---|---|---|
| Context Retrieval | Gateway key | x-gateway-key |
Chat Engine |
| Ingestion | Gateway key | x-gateway-key |
Chat Engine |
| Contact Management | JWT (user session) | Authorization: Bearer <token> |
AI Brain / Dashboard |
| Outreach Management | JWT (user session) | Authorization: Bearer <token> |
AI Brain / Dashboard |
| Internal (Cron, Queues) | Gateway key | x-gateway-key |
CF Cron / CF Queues |
3. Endpoints by Domain
3.1 Context & Ingestion (Chat Engine — Hot Path)
Get Contact Context
The primary hot-path endpoint. Called by Chat Engine before generating a response. Returns the L1+L2 context packet: behavioral guidance, message relevance, relationship state, crisis level, and pending outreach triggers.
GET /contact-intel/context/:contact_id
x-gateway-key: <key>Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
session_count |
Integer | 0 | Number of sessions this contact has had. Determines cold-start vs. real context. |
message |
String ≤500 | — | Current user message. When present, triggers L2 semantic search and entity expansion. |
bot_id |
String | — | Bot identifier. Used to fetch a bot-specific cold-start brief from Chat Engine (Phase 5). |
Response 200:
{
"session_core": "Arjun is a software engineer who chats late at night. He has a dog named Bruno. He appreciates humor but needs direct support during work stress.",
"message_relevance": "Relevant: Bruno had a vet appointment last week. Also known: Infosys, sprint deadline",
"has_message_context": true,
"relationship": {
"stage": "established",
"trajectory": "moving_up",
"velocity": "moderate",
"mood_trend": "stable"
},
"crisis_level": "none",
"triggers": [
{
"triggerType": "inactivity",
"message": "Hey! It's been a while — how's Bruno doing?",
"context": { "days_inactive": 5 }
}
]
}Field Reference:
| Field | Type | Description |
|---|---|---|
session_core |
string | null | L1 behavioral guidance. Generic cold-start text when session_count < cold_start_used_until (default 5). contacts.core_summary for established contacts; null until first weekly consolidation runs. |
message_relevance |
string | null | L2 result: most relevant memory + entity neighbors for the current message. Null if no message param or no semantic hits above 0.7 cosine similarity. |
has_message_context |
boolean | Whether L2 found anything useful for this message. |
relationship.stage |
enum | new | building | established | deep | fading | dormant |
relationship.trajectory |
enum | null | moving_up | stable | moving_down | volatile — from latest weekly relationship snapshot |
relationship.velocity |
enum | null | slow | moderate | fast — rate of relationship change |
relationship.mood_trend |
enum | null | improving | declining | stable | volatile — from latest weekly emotional snapshot |
crisis_level |
enum | none | distress | crisis — gates CE safety escalation |
triggers |
array | Pending outreach triggers whose scheduled_at has passed. Max 3. Empty array when none. |
Backward-Compat Mood Endpoint (remove in Phase 5):
GET /contact-intel/context/:contact_id/moodReturns the legacy { mood, mood_line, energy, relationship_stage, ... } shape. Chat Engine still calls this until the Phase 5 orchestrator update. Do not use for new consumers.
Error Codes:
| Code | HTTP | Description |
|---|---|---|
CONTACT_NOT_FOUND |
404 | Contact ID does not exist for this tenant |
Performance SLA: < 30ms p95 (5 parallel queries)
Ingest Interaction
Called by Chat Engine on every message. Runs mood classification, rules-based memory extraction, entity detection, and contact state update synchronously before returning 202. When session_close: true, also runs the LLM batch session-close extraction inline.
POST /contact-intel/ingest
x-gateway-key: <key>
Content-Type: application/jsonRequest Body:
{
"contact_id": "ct_arjun",
"conversation_id": "conv_abc123",
"message": "Bruno ate my shoes again",
"role": "user",
"session_close": false,
"session_id": "sess_xyz",
"session_messages": []
}| Field | Type | Required | Description |
|---|---|---|---|
contact_id |
UUID | ✅ | Contact to ingest for |
message |
String | ✅ | The message text |
role |
Enum | ✅ | user or assistant. Mood + memory extraction only run for user messages. |
conversation_id |
UUID | — | CE conversation ID. Stored as source_id on extracted memories. |
session_close |
Boolean | — | Default false. When true, triggers the LLM batch extraction (requires session_messages). |
session_id |
String | — | CE session ID. Stored as source_session_id on LLM-extracted memories for provenance. |
session_messages |
Array | — | Full transcript [{ role, content }]. Required when session_close: true. |
Response 202:
{
"status": "accepted",
"memories_extracted": 2,
"entities_extracted": 1,
"mood": { "mood": "frustrated", "confidence": 0.88, "energy": "medium" },
"contact_id": "ct_arjun"
}Note: memories_extracted counts only inline rules-based extractions, not session-close LLM extractions. Embeddings are generated asynchronously via waitUntil() — the first GET /context/:id?message=... after ingest may return message_relevance: null.
Performance SLA: < 50ms for non-session-close. Session-close adds ~2–5s for the LLM call.
3.2 Contact Management (AI Brain / Dashboard)
Create Contact
Creates a new contact profile. Idempotent — returns the existing contact (with created: false) if (tenant_id, external_id, channel) already exists.
POST /contact-intel/contacts
x-gateway-key: <key>
Content-Type: application/json| Field | Type | Required | Description |
|---|---|---|---|
external_id |
String | ✅ | Channel-specific ID (WhatsApp phone, Telegram chat ID, widget session) |
channel |
Enum | ✅ | whatsapp | telegram | widget | instagram |
display_name |
String | — | Contact name |
Response 201 (new) / 200 (existing):
{ "contact": { "id": "ct_arjun", "external_id": "...", "channel": "whatsapp" }, "created": true }List Contacts
GET /contact-intel/contacts?sort=churn_risk&stage=fading&limit=20&offset=0| Param | Type | Default | Description |
|---|---|---|---|
sort |
Enum | recent |
recent, churn_risk, messages, created |
stage |
Enum | all |
Filter: new, building, established, deep, fading, dormant, all |
channel |
Enum | all |
Filter: whatsapp, telegram, widget, instagram, all |
limit |
Integer | 20 | Max results (1–100) |
offset |
Integer | 0 | Pagination offset |
Response 200:
{
"contacts": [{ "id": "...", "displayName": "Arjun", "relationshipStage": "established", "churnRisk": 0.12, ... }],
"total": 234,
"limit": 20,
"offset": 0
}Get Contact Profile
GET /contact-intel/contacts/:id/profileResponse 200: { contact, state, memories (top 50 by importance), entities }
Delete Contact (GDPR)
Soft-deletes the contact row (scrubs PII), hard-deletes memories, legacy entities, and interaction logs. New CI tables (ci_entities, ci_emotional_snapshots, ci_relationship_snapshots, ci_contact_channels) cascade on contact FK delete.
DELETE /contact-intel/contacts/:idResponse 200: { "deleted": true }
Export Contact Data (GDPR)
GET /contact-intel/contacts/:id/exportResponse 200:
{
"export_date": "2026-06-29T00:00:00.000Z",
"contact": { ... },
"state": { ... },
"memories": [ ... ],
"entities": [ ... ],
"interaction_count": 245
}List Contact Memories
GET /contact-intel/contacts/:id/memoriesReturns up to 100 memories (active + superseded) ordered by importance descending.
Response 200: { "memories": [{ id, memoryType, content, importance, accessCount, entityIds, createdAt, accessedAt }] }
Delete All Memories
DELETE /contact-intel/contacts/:id/memoriesHard-deletes all memories for the contact. Response 200: { "deleted": true }
Correct a Memory
Supersedes an incorrect memory with a correction. The new memory gets source: "contact_corrected" and confidence: 1.0. Also nullifies core_summary to force regeneration on next weekly consolidation.
POST /contact-intel/contacts/:id/correct
Content-Type: application/json| Field | Type | Required | Description |
|---|---|---|---|
hint |
String ≤500 | ✅ | Fragment of the incorrect memory (ILIKE search) |
correction |
String ≤2000 | ✅ | The correct information to store |
Response 200: { "corrected": true, "memory_id": "uuid" }
Response 404: { "error": { "code": "MEMORY_NOT_FOUND" } } — when no active memory matches the hint
3.3 Outreach Management
Schedule Outreach
POST /contact-intel/outreach
Content-Type: application/jsonRequest Body:
{
"contact_id": "ct_arjun",
"trigger_type": "scheduled",
"scheduled_at": "2026-06-30T08:00:00Z",
"message": "Hey! Hope the sprint is going well.",
"context": { "event": "sprint_deadline" }
}Response 201: { "id": "trg_789", "status": "pending", "scheduled_at": "..." }
List Outreach Triggers
GET /contact-intel/outreach?contact_id=ct_arjun&status=pending| Param | Type | Default | Description |
|---|---|---|---|
contact_id |
UUID | — | Filter by contact |
status |
Enum | all |
pending, fired, failed, cancelled, expired, all |
trigger_type |
Enum | all |
scheduled, inactivity, milestone, recurring, all |
limit |
Integer | 20 | Max results |
Cancel Outreach Trigger
POST /contact-intel/outreach/:id/cancelResponse 200: { "cancelled": true }
Get Outreach Config
GET /contact-intel/outreach/configResponse 200:
{
"enabled": true,
"inactivityDays": 3,
"maxInactivityAttempts": 2,
"minGapDays": 3,
"maxPerDay": 3,
"quietHoursStart": 22,
"quietHoursEnd": 8,
"timezone": "Asia/Kolkata"
}Update Outreach Config
PATCH /contact-intel/outreach/config
Content-Type: application/jsonSend any subset of the config fields to update. Returns the updated config.
3.4 Internal Endpoints (Cron & Queue)
Scan Outreach Triggers (Cron — every 6h)
POST /internal/outreach/scan
x-gateway-key: <key>Finds pending triggers whose scheduled_at has passed, generates messages, marks them fired. Also creates inactivity triggers for contacts exceeding inactivity_days.
Process Weekly Consolidation (Queue Consumer)
The Sunday 2am UTC cron enqueues one message per active contact to CONSOLIDATION_QUEUE. The queue consumer runs the 9-step consolidateContact() pipeline for each contact. See contact-memory-spec.md for the full pipeline.
4. Error Response Format
{
"error": {
"code": "CONTACT_NOT_FOUND",
"message": "Contact ct_xyz does not exist for this tenant."
}
}Standard Error Codes:
| Code | HTTP | Description |
|---|---|---|
CONTACT_NOT_FOUND |
404 | Contact ID does not exist for this tenant |
MEMORY_NOT_FOUND |
404 | No active memory matches the given hint |
TRIGGER_NOT_FOUND |
404 | Outreach trigger ID does not exist |
INVALID_INPUT |
400 | Malformed request body |
PERMISSION_DENIED |
403 | User lacks required PBAC permission |
QUIET_HOURS |
409 | Cannot schedule outreach during configured quiet hours |
MAX_ATTEMPTS_REACHED |
409 | Inactivity outreach limit already reached for this contact |