CRM API Reference

Contacts, companies, deals, activities, tasks, and automated email sequences. Multi-tenant, EU jurisdiction, zero external dependencies.

Internal API. This CRM is Pauhu’s internal sales tool. It is not part of the customer-facing product. All data is stored in EU jurisdiction (D1 database: 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.

ParameterTypeDescription
limitintegerMax results (default: 50, max: 100)
offsetintegerPagination offset (default: 0)
stagestringFilter by lifecycle stage: subscriber, lead, mql, sql, opportunity, customer
qstringSearch 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:

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

FieldTypeDefault
emailstringnull
first_namestringnull
last_namestringnull
phonestringnull
titlestringnull
company_idUUIDnull
lifecycle_stagestringsubscriber
lead_scoreinteger0
lead_sourcestringnull
owner_idstringnull
tagsstring[][]
custom_fieldsobject{}

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.

ParameterTypeDescription
limitintegerMax results (default: 50, max: 100)
offsetintegerPagination offset
targetbooleantrue to show target accounts only
qstringSearch 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

FieldTypeDefault
namestringUnknown Company
domainstringnull
industrystringnull
sizestringnull
countrystringnull (ISO 3166-1 alpha-2)
citystringnull
websitestringnull
linkedin_urlstringnull
descriptionstringnull
icp_tierstringnull (tier-1, tier-2, tier-3)
is_target_accountbooleanfalse
owner_idstringnull
tagsstring[][]
custom_fieldsobject{}

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.

ParameterTypeDescription
limitintegerMax results (default: 50, max: 100)
offsetintegerPagination offset
stagestringFilter 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

FieldTypeDefault
namestringNew Deal
company_idUUIDnull
contact_idUUIDnull
stagestringlead
amountnumbernull (EUR)
currencystringEUR
probabilityintegernull (0–100)
expected_close_datedatenull (ISO 8601)
owner_idstringnull

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.

ParameterTypeDescription
limitintegerMax results (default: 50, max: 100)
contact_idUUIDFilter by contact
company_idUUIDFilter by company
typestringnote, 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

FieldTypeDefault
contact_idUUIDnull
company_idUUIDnull
deal_idUUIDnull
typestringnote
subjectstringnull
bodystringnull
metadataobject{}
occurred_atdatetimenow (ISO 8601)
created_bystringnull

6. Tasks

GET /v1/crm/tasks

List tasks with optional filtering.

ParameterTypeDescription
limitintegerMax results (default: 50, max: 100)
statusstringtodo, in_progress, done
assigned_tostringFilter by assignee
overduebooleantrue 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.

SequenceSegmentStepsDelays (days)
seq-hot-sovereignICP 70+30, 2, 5
seq-warm-regulatoryICP 40–6950, 4, 8, 14, 21
seq-cold-educationICP < 4030, 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)

  1. Processes EU regulatory data (EUR-Lex, TED, IATE, CURIA)
  2. Has GitHub presence (technically capable, open source friendly)
  3. Has sovereign/on-premise requirements (GDPR, NIS2, data residency)
  4. Has procurement budget for AI/search tools
  5. Needs multilingual search (24 EU languages)

Segments

SegmentScore RangeAuto-enrolled Sequence
Hot70–100seq-hot-sovereign
Warm40–69seq-warm-regulatory
Cold1–39seq-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:

# 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

All Documentation · sales@pauhu.eu