ICP rubrics
An ideal customer profile, written by you as a short list of weighted criteria and scored by arithmetic. A model helps you write the rubric; it never produces the number.
Why a rubric and not a score from a model
A model that says a lead is a 73 cannot tell you why, cannot be replayed, and cannot be argued with. A rubric that says “industry match 30 points, 50–500 employees 25, director or above 20” can be read by a sales rep, changed in a minute, and checked against what your signups went on to do. The same rubric over the same facts is the same number, every time.
Endpoints
| Method | Path | Does |
|---|---|---|
POST | /v1/icp/compile | { text } — English in, candidate rubric out. Saves nothing. |
GET | /v1/icp | { active, versions } — the active rubric and every version's number, author and date. |
POST | /v1/icp | Save a rubric version. Activates it unless you send activate: false. 201 with any warnings. |
POST | /v1/icp/activate | { version } — make an earlier or later version active. |
The rubric
POST /v1/icp
{
"criteria": [
{ "id": "c_industry", "label": "Fintech or insurance", "weight": 30,
"when": { "field": "enrichment.company.industry", "op": "in", "value": ["fintech", "insurance"] } },
{ "id": "c_size", "label": "50–500 employees", "weight": 25,
"when": { "all": [
{ "field": "enrichment.company.employees", "op": "gte", "value": 50 },
{ "field": "enrichment.company.employees", "op": "lte", "value": 500 } ] } },
{ "id": "c_seniority", "label": "Director or above", "weight": 25,
"when": { "field": "enrichment.person.seniority", "op": "in", "value": ["director", "vp", "c_suite"] } },
{ "id": "c_geo", "label": "US or Canada", "weight": 20,
"when": { "field": "enrichment.company.country", "op": "in", "value": ["US", "CA"] } },
{ "id": "c_tech", "label": "Uses HubSpot (bonus)", "weight": 10, "bonus": true,
"when": { "field": "enrichment.company.tech", "op": "in", "value": ["hubspot"] } }
],
"thresholds": { "strong": 75, "fit": 55, "weak": 30 },
"source_text": "We sell to fintech and insurance companies...",
"activate": true
}- Criteria use the same predicates and the same field vocabulary as policy rules, except that a criterion may not read
icp.*— a score cannot depend on itself. - A
bonuscriterion adds points but is left out of coverage, so a missing nice-to-have does not make a score look less trustworthy. - Saving warns, without failing, when the non-bonus weights do not sum to 100 or when the strong threshold is higher than any score the rubric can reach.
The score
Each criterion that matches earns its weight. A miss or an unknown earns nothing. The total is rounded and clamped to 0–100, so bonus points can lift a score to 100 but not past it.
"icp": {
"score": 55,
"fit": "fit",
"coverage": 0.75,
"version": 3,
"criteria": [
{ "id": "c_industry", "result": "match", "points": 30 },
{ "id": "c_size", "result": "match", "points": 25 },
{ "id": "c_seniority", "result": "unknown", "points": 0 },
{ "id": "c_geo", "result": "miss", "points": 0 },
{ "id": "c_tech", "result": "miss", "points": 0 }
]
}Example values. Seniority was unknown, so 25 of the 100 non-bonus points could not be evaluated: coverage 0.75.
| Field | Type | Meaning |
|---|---|---|
score | number | 0–100, rounded and clamped. The sum of the weights of the criteria that matched. |
fit | enum | strong, fit, weak or no_fit, from the thresholds. Coverage does not move the band. |
coverage | number | 0–1. The share of non-bonus weight whose criteria could be evaluated. Mandatory; read it every time. |
version | number | The rubric version that produced the score. |
criteria | array | { id, result, points } per criterion. result is match, miss or unknown. |
The coverage rule
A 55 at coverage 0.95 and a 55 at coverage 0.3 are different facts. The second one mostly measures what the enrichment vendors did not know. Coverage is on every score, and a policy can read it, so the rule you probably want looks like this:
{
"id": "r_low_fit",
"label": "Review low ICP fit, only when we actually know enough",
"when": { "all": [
{ "field": "icp.score", "op": "lt", "value": 55 },
{ "field": "icp.coverage", "op": "gte", "value": 0.7 }
] },
"then": { "action": "review" },
"on_unknown": "skip"
}ICP inside a decision
- When your account has an active rubric, every /v1/decide response carries an
icpscore. With no rubric it isnull. - Having a rubric does not, by itself, buy enrichment. A profile is bought when evaluation reaches a policy rule that reads an
icp.*or enrichment field (or you passenrich: "always"). Otherwise the score is computed over what is already known, and its coverage will say how little that is. - Whether a high score actually converts better is a question for your outcomes. Once events or Stripe are flowing,
GET /v1/segmentsreports conversion byicp.fit; ifstrongdoes not out-convertno_fit, the rubric is wrong. See events.
Limits
| What | Limit |
|---|---|
| Criteria per rubric | 20 |
| Criterion id | c_ followed by lowercase letters, digits and _ |
| Weight | Greater than 0, at most 100 |
| Thresholds | Each 0–100, strictly ordered strong > fit > weak |
| Predicate depth / nodes | 8 / 100 |
| Values in an in / not_in set | 100 |
| Label | 200 characters |
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_request | Missing or empty text, or a bad version number. |
400 | policy_invalid | The rubric fails validation: an unknown field or value, a criterion reading icp.*, bad weights or thresholds. The first five errors are in the message. |
404 | not_found | No such version. |
422 | compile_failed | The compiler could not produce a valid rubric, or is not configured on this deployment. error.questions carries what it needs answered. |