CRM API Reference
Contacts, companies, deals, activities, tasks, and automated email sequences. Multi-tenant, EU jurisdiction, zero external dependencies.
pauhu-crm-eu).
1. Authentication
All CRM endpoints require a workspace header for multi-tenant isolation:
X-Workspace-ID: ws-pauhu
Requests without X-Workspace-ID return 401. CORS is restricted to allowed Pauhu origins.
Base URL
CRM_BASE=https://staging.pauhu.eu
2. Contacts
GET /v1/crm/contacts
List contacts with optional filtering and pagination.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default: 50, max: 100) |
offset | integer | Pagination offset (default: 0) |
stage | string | Filter by lifecycle stage: subscriber, lead, mql, sql, opportunity, customer |
q | string | Search by email, first name, or last name |
# List all contacts
curl -s "$CRM_BASE/v1/crm/contacts" \
-H "X-Workspace-ID: ws-pauhu" | jq
# Search contacts by name
curl -s "$CRM_BASE/v1/crm/contacts?q=linda" \
-H "X-Workspace-ID: ws-pauhu" | jq
# Filter by lifecycle stage
curl -s "$CRM_BASE/v1/crm/contacts?stage=sql" \
-H "X-Workspace-ID: ws-pauhu" | jq
GET /v1/crm/contacts/:id
Get a single contact with linked company, activities, and deals.
curl -s "$CRM_BASE/v1/crm/contacts/abc-123" \
-H "X-Workspace-ID: ws-pauhu" | jq
Response includes:
contact— full contact recordcompany— linked company (ifcompany_idis set)activities— last 20 activitiesdeals— all deals for this contact
POST /v1/crm/contacts
Create a new contact. Duplicate emails return 409.
curl -s -X POST "$CRM_BASE/v1/crm/contacts" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{
"email": "sanna.virtanen@dvv.fi",
"first_name": "Sanna",
"last_name": "Virtanen",
"title": "Chief Data Officer",
"company_id": "comp-dvv",
"lifecycle_stage": "lead",
"lead_source": "conference",
"tags": ["gov-fi", "data-governance"]
}' | jq
Contact fields
| Field | Type | Default |
|---|---|---|
email | string | null |
first_name | string | null |
last_name | string | null |
phone | string | null |
title | string | null |
company_id | UUID | null |
lifecycle_stage | string | subscriber |
lead_score | integer | 0 |
lead_source | string | null |
owner_id | string | null |
tags | string[] | [] |
custom_fields | object | {} |
PUT /v1/crm/contacts/:id
Update a contact. Only provided fields are modified.
curl -s -X PUT "$CRM_BASE/v1/crm/contacts/abc-123" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{"lifecycle_stage": "sql", "lead_score": 85}' | jq
DELETE /v1/crm/contacts/:id
Delete a contact. Returns 404 if not found.
curl -s -X DELETE "$CRM_BASE/v1/crm/contacts/abc-123" \
-H "X-Workspace-ID: ws-pauhu" | jq
3. Companies
GET /v1/crm/companies
List companies with optional filtering.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default: 50, max: 100) |
offset | integer | Pagination offset |
target | boolean | true to show target accounts only |
q | string | Search by name or domain |
# List target accounts
curl -s "$CRM_BASE/v1/crm/companies?target=true" \
-H "X-Workspace-ID: ws-pauhu" | jq
# Search by domain
curl -s "$CRM_BASE/v1/crm/companies?q=europa.eu" \
-H "X-Workspace-ID: ws-pauhu" | jq
GET /v1/crm/companies/:id
Get a company with linked contacts, deals, and activities.
curl -s "$CRM_BASE/v1/crm/companies/comp-dvv" \
-H "X-Workspace-ID: ws-pauhu" | jq
POST /v1/crm/companies
Create a company. Duplicate domains return 409.
curl -s -X POST "$CRM_BASE/v1/crm/companies" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{
"name": "Digital and Population Data Services Agency",
"domain": "dvv.fi",
"industry": "government",
"size": "1000+",
"country": "FI",
"city": "Helsinki",
"website": "https://dvv.fi",
"icp_tier": "tier-1",
"is_target_account": true,
"tags": ["gov-fi", "digital-services"]
}' | jq
Company fields
| Field | Type | Default |
|---|---|---|
name | string | Unknown Company |
domain | string | null |
industry | string | null |
size | string | null |
country | string | null (ISO 3166-1 alpha-2) |
city | string | null |
website | string | null |
linkedin_url | string | null |
description | string | null |
icp_tier | string | null (tier-1, tier-2, tier-3) |
is_target_account | boolean | false |
owner_id | string | null |
tags | string[] | [] |
custom_fields | object | {} |
PUT /v1/crm/companies/:id
Update a company. Only provided fields are modified.
curl -s -X PUT "$CRM_BASE/v1/crm/companies/comp-dvv" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{"icp_tier": "tier-1", "is_target_account": true}' | jq
4. Deals
GET /v1/crm/deals
List deals with pipeline summary.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default: 50, max: 100) |
offset | integer | Pagination offset |
stage | string | Filter by stage: lead, qualified, proposal, negotiation, closed_won, closed_lost |
Response includes a pipeline array with per-stage counts and totals:
curl -s "$CRM_BASE/v1/crm/deals" \
-H "X-Workspace-ID: ws-pauhu" | jq '.pipeline'
# Example response:
# [
# {"stage": "lead", "count": 3, "total_amount": 180000},
# {"stage": "qualified", "count": 4, "total_amount": 520000},
# {"stage": "proposal", "count": 2, "total_amount": 350000},
# {"stage": "negotiation", "count": 1, "total_amount": 150000}
# ]
GET /v1/crm/deals/:id
Get a deal with linked company, contact, and activities.
POST /v1/crm/deals
Create a deal.
curl -s -X POST "$CRM_BASE/v1/crm/deals" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{
"name": "DVV Sovereign Container - 3yr",
"company_id": "comp-dvv",
"contact_id": "abc-123",
"stage": "qualified",
"amount": 150000,
"currency": "EUR",
"probability": 40,
"expected_close_date": "2026-09-01"
}' | jq
Deal fields
| Field | Type | Default |
|---|---|---|
name | string | New Deal |
company_id | UUID | null |
contact_id | UUID | null |
stage | string | lead |
amount | number | null (EUR) |
currency | string | EUR |
probability | integer | null (0–100) |
expected_close_date | date | null (ISO 8601) |
owner_id | string | null |
PUT /v1/crm/deals/:id
Update a deal. Additional fields for closing: actual_close_date, lost_reason.
# Move deal to negotiation
curl -s -X PUT "$CRM_BASE/v1/crm/deals/deal-123" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{"stage": "negotiation", "probability": 70}' | jq
5. Activities
GET /v1/crm/activities
List activities (timeline). Filter by contact, company, deal, or type.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default: 50, max: 100) |
contact_id | UUID | Filter by contact |
company_id | UUID | Filter by company |
type | string | note, email, call, meeting, task |
# Get all activities for a contact
curl -s "$CRM_BASE/v1/crm/activities?contact_id=abc-123" \
-H "X-Workspace-ID: ws-pauhu" | jq
POST /v1/crm/activities
Log an activity. Automatically updates last_activity_at on the linked contact.
curl -s -X POST "$CRM_BASE/v1/crm/activities" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "abc-123",
"company_id": "comp-dvv",
"type": "meeting",
"subject": "Sovereign container demo",
"body": "Demonstrated 8-container deployment. DVV interested in air-gapped mode.",
"occurred_at": "2026-03-07T10:00:00Z"
}' | jq
Activity fields
| Field | Type | Default |
|---|---|---|
contact_id | UUID | null |
company_id | UUID | null |
deal_id | UUID | null |
type | string | note |
subject | string | null |
body | string | null |
metadata | object | {} |
occurred_at | datetime | now (ISO 8601) |
created_by | string | null |
6. Tasks
GET /v1/crm/tasks
List tasks with optional filtering.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Max results (default: 50, max: 100) |
status | string | todo, in_progress, done |
assigned_to | string | Filter by assignee |
overdue | boolean | true to show overdue tasks only |
# List overdue tasks
curl -s "$CRM_BASE/v1/crm/tasks?overdue=true" \
-H "X-Workspace-ID: ws-pauhu" | jq
POST /v1/crm/tasks
Create a task linked to a contact, company, or deal.
curl -s -X POST "$CRM_BASE/v1/crm/tasks" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "abc-123",
"title": "Send sovereign deployment proposal",
"due_date": "2026-03-14",
"priority": "high",
"assigned_to": "linda"
}' | jq
PUT /v1/crm/tasks/:id
Update a task. Setting status: "done" automatically records completed_at.
curl -s -X PUT "$CRM_BASE/v1/crm/tasks/task-123" \
-H "X-Workspace-ID: ws-pauhu" \
-H "Content-Type: application/json" \
-d '{"status": "done"}' | jq
7. Email Sequences
Automated email sequences are managed via a separate service. Contacts are auto-enrolled based on their AI lead score segment.
| Sequence | Segment | Steps | Delays (days) |
|---|---|---|---|
seq-hot-sovereign | ICP 70+ | 3 | 0, 2, 5 |
seq-warm-regulatory | ICP 40–69 | 5 | 0, 4, 8, 14, 21 |
seq-cold-education | ICP < 40 | 3 | 0, 7, 14 |
GET /v1/sequences
List all email sequences.
SEQ_BASE=https://staging.pauhu.eu
curl -s "$SEQ_BASE/v1/sequences" \
-H "X-Workspace-ID: ws-pauhu" | jq
GET /v1/sequences/:id
Get sequence details including steps, delays, and enrollment counts.
POST /v1/sequences
Create a new email sequence.
POST /run
Manually trigger the sequence engine (normally runs via cron at 08:00 weekdays).
curl -s -X POST "$SEQ_BASE/run" \
-H "X-Workspace-ID: ws-pauhu" | jq
8. AI Lead Scoring
Lead scoring uses AI to evaluate each contact’s company against 5 criteria and assigns a score from 1 to 100. Scoring runs automatically via daily cron, or can be triggered manually.
Scoring criteria (equal weight)
- Processes EU regulatory data (EUR-Lex, TED, IATE, CURIA)
- Has GitHub presence (technically capable, open source friendly)
- Has sovereign/on-premise requirements (GDPR, NIS2, data residency)
- Has procurement budget for AI/search tools
- Needs multilingual search (24 EU languages)
Segments
| Segment | Score Range | Auto-enrolled Sequence |
|---|---|---|
| Hot | 70–100 | seq-hot-sovereign |
| Warm | 40–69 | seq-warm-regulatory |
| Cold | 1–39 | seq-cold-education |
POST /score
Manually trigger AI scoring for all unscored contacts.
SCORE_BASE=https://staging.pauhu.eu
curl -s -X POST "$SCORE_BASE/score" \
-H "X-Workspace-ID: ws-pauhu" | jq
# Response:
# {"scored": 12, "errors": 0}
GET /dashboard
Scoring dashboard with segment distribution and top prospects.
9. Admin UI
A single-page admin UI is served at /admin on the CRM base URL. It includes:
- Dashboard: Pipeline value, scoring segments, top prospects
- Pipeline board: Kanban view of deals across stages
- Contacts: Searchable list with score badges and detail panel
- Companies: ICP tier, target account flags, GitHub links
- Sequences: Enrollment counts, step configuration
- Tasks: Overdue tracking, assignee filter
- Activity timeline: All interactions across contacts and deals
# Open admin UI in browser
open "$CRM_BASE/admin"
Health Check
All three services expose a /health endpoint:
curl -s "$CRM_BASE/health" | jq
# {"service":"crm","jurisdiction":"eu","status":"healthy","timestamp":"..."}
curl -s "$SCORE_BASE/health" | jq
curl -s "$SEQ_BASE/health" | jq