Skip to main content

Lead Scoring & Timeline

Score contacts and companies from behavioural signals with your own rules, read the per-rule breakdown, and pull a unified activity timeline.

Overview

Lead scoring turns behaviour into a number. You define rules — each a signal, an operator, a value and a point award — and the API sums the ones that match a record, producing a score and an A–D grade with a per-rule breakdown so you can see exactly why.

The timeline is the other half of the same question: a single merged feed of everything that has happened to a contact or company.

Authentication

Authorization: Bearer cm_your_api_key
OperationScope
Read rules and scorescrm_scores:read
Create/update/delete rules, recomputecrm_scores:write
Read the timelinecrm_timeline:read

Lead scoring

Signals

Signals are a closed allowlist per entity type — an unknown signal is 422, never a silently-false rule.

Contact

SignalKind
has_email, has_phone, whatsapp_opt_in, email_opt_in, sms_opt_in, has_companyboolean
conversation_count, deal_count, deal_value_total, days_since_last_conversationnumber

Company

SignalKind
has_domain, has_phoneboolean
industry, sizestring
contact_count, deal_count, deal_value_totalnumber

Operators

eq, neq, gt, gte, lt, lte, contains, is_set, is_not_set — but which ones are legal depends on the signal's kind:

KindAllowed operators
booleaneq, neq, is_set, is_not_set
numbereq, neq, gt, gte, lt, lte, is_set, is_not_set
stringeq, neq, contains, is_set, is_not_set

is_set and is_not_set take no value — one sent is dropped.

Grades

GradeScore
A75 and above
B50–74
C25–49
Dbelow 25

The score is the plain sum of the points of every active rule that matched. It is not clamped, so negative rules can take it below zero.

The rule object

{
  "id": "77aa…",
  "tenant_id": "a0b1…",
  "entity_type": "contact",
  "name": "Opted in on WhatsApp",
  "signal": "whatsapp_opt_in",
  "operator": "eq",
  "value": true,
  "points": 20,
  "is_active": true,
  "created_at": "2026-08-06T09:00:00Z",
  "updated_at": "2026-08-06T09:00:00Z"
}

GET /api/v1/crm/lead-scores/rules

Oldest first — the order you built them in.

ParameterTypeConstraints
entity_typestringcontact or company
is_activeboolean
limitinteger1 <= limit <= 200, default 50
offsetinteger0 <= offset <= 100000, default 0

POST /api/v1/crm/lead-scores/rules

FieldTypeRequiredConstraints
entity_typestringYescontact or company
namestringYes1–255 characters, unique per entity type
signalstringYesFrom the allowlist for that entity type
operatorstringYesLegal for the signal's kind
valueanyConditionalRequired unless the operator is is_set / is_not_set. String values at most 255 characters
pointsintegerYes-1000 <= points <= 1000. Negative points subtract
is_activebooleanNoDefault true
curl -X POST https://api.callmissed.com/api/v1/crm/lead-scores/rules \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "contact",
    "name": "Stale — no contact in 30 days",
    "signal": "days_since_last_conversation",
    "operator": "gt",
    "value": 30,
    "points": -15
  }'

Returns 201. A name already used for that entity type returns 409 A contact scoring rule named '…' already exists.

Errors name the exact problem, for example operator 'contains' is not valid for the boolean signal 'has_email'. Allowed: eq, neq, is_set, is_not_set.

PATCH / DELETE /api/v1/crm/lead-scores/rules/{rule_id}

PATCH takes every field except entity_type, all optional. DELETE returns 204; both return 404 Scoring rule not found for an id outside your tenant. The signal / operator / value triple is re-validated against the merged result, so you cannot change the signal in one call and leave an illegal operator behind.


Reading scores

GET /api/v1/crm/lead-scores

Highest score first — the ranked list.

ParameterTypeConstraints
entity_typestringcontact or company
limitinteger1 <= limit <= 200, default 50
offsetinteger0 <= offset <= 100000, default 0

GET /api/v1/crm/lead-scores/{entity_type}/{entity_id}

{
  "id": "88bb…",
  "tenant_id": "a0b1…",
  "entity_type": "contact",
  "entity_id": "4411…",
  "score": 62,
  "grade": "B",
  "breakdown": [
    { "rule_id": "77aa…", "name": "Opted in on WhatsApp", "signal": "whatsapp_opt_in", "operator": "eq", "value": true, "points": 20 },
    { "rule_id": "99cc…", "name": "Has an open deal", "signal": "deal_count", "operator": "gte", "value": 1, "points": 42 }
  ],
  "computed_at": "2026-08-17T05:00:00Z"
}

breakdown lists only the rules that matched, which is what makes a score explainable to a salesperson.

404 No score has been computed for this record when the record has never been scored — call recompute first.

POST /api/v1/crm/lead-scores/recompute

FieldTypeRequiredConstraints
entity_typestringYescontact or company
entity_idsUUID[]Yes1–200 entries
{ "entity_type": "contact", "requested": 3, "computed": 3 }

Idempotent: recomputing overwrites the record's previous score. Repeated ids are collapsed, and requested is the raw count you sent. If any id is not yours you get 404 2 of 50 contact ids were not found in this tenant; nothing was scored — nothing is partially computed.

Scores are not recomputed automatically after you change a rule. Re-run the affected records yourself.


Timeline

GET /api/v1/crm/timeline

A merged, newest-first feed of everything attached to one record. A company's feed also includes the activity of its contacts.

ParameterTypeRequiredConstraints
entity_typestringYescontact or company. Deals are not supported here
entity_idUUIDYes
typesstringNoAt most 128 characters. Comma-separated subset of conversation,message,call,note,task,deal. Omit for all
limitintegerNo1 <= limit <= 200, default 50
offsetintegerNo0 <= offset <= 100000, default 0
curl "https://api.callmissed.com/api/v1/crm/timeline?entity_type=contact&entity_id=4411…&types=note,task" \
  -H "Authorization: Bearer cm_your_api_key"
[
  {
    "id": "note:aa22…",
    "type": "note",
    "occurred_at": "2026-08-16T12:00:00Z",
    "title": "Note",
    "summary": "Renewal call went well — wants a Hindi voice agent.",
    "actor": "b1f2…",
    "ref_id": "aa22…",
    "meta": {}
  }
]
FieldTypeNotes
idstringPrefixed composite such as note:<uuid> — unique across types, good as a render key
ref_idUUIDThe underlying record's own id, for a follow-up fetch
summarystring | nullTruncated to 280 characters
actorstring | nullWho caused it, when known: a user id for notes, tasks and deals, the sender's role for messages, the other party's number for calls. null when there is no attributable actor
metaobjectType-specific extras. Defaults to {}

An unknown or another tenant's entity_id returns an empty array, not a 404. The timeline never confirms whether a record exists — do not use it as an existence check.

Read-only: there is no write scope and no way to post to a timeline. It is assembled from the underlying records.


Errors

StatusWhen
403Key is missing crm_scores:* / crm_timeline:read
404Rule not found, no score computed yet, or an id outside your tenant during recompute
409Duplicate rule name
422Unknown signal, an operator illegal for that signal's kind, a value of the wrong type, points outside -1000..1000, over 200 recompute ids, or an unknown timeline type

Nothing on this page consumes credits.