Sales Disposition API
Capture how a sales rep actually felt about a lead after working it. A disposition is a one-tap outcome ("Qualified", "Not ICP", "Nurture", and so on) that your team records on a lead. Dispositions close the loop between scoring and reality: they tell kenbun which leads your reps agreed were worth pursuing, so your workspace's scoring can be calibrated against real sales judgment.
Reps can set a disposition from three places: this API, the Log Outcome menu on a lead's Slack alert, and the lead detail page under Analyze > Leads.
Why Dispositions Matter
Scoring tells your team who looks promising. Dispositions tell you who was promising. When a rep marks a lead Qualified (SQL) or Closed Won, kenbun records the lead's score and the scoring rules in effect at that moment. Over time, this builds a labeled history of outcomes that the calibration engine uses to suggest better rule weights.
Dispositions never change your scoring weights on their own. They are feedback, not automation. Suggested weight changes always require a person to review and apply them under Configure > Scoring.
Understanding the Six Outcomes
Each disposition has a plain-language label your team sees throughout the app:
| Value | Label shown to your team | Meaning |
|---|---|---|
accepted_sql | Qualified (SQL) | The rep accepted the lead as a Sales Qualified Lead |
rejected_not_icp | Rejected (Not ICP) | The lead is not a fit for your ideal customer profile |
rejected_no_response | Rejected (No Response) | The rep could not reach the lead |
nurture | Nurture | Not ready now; send back to marketing nurture |
closed_won | Closed Won | The deal closed successfully |
closed_lost | Closed Lost | The deal was lost |
A lead can also have no disposition (the default). Clearing a disposition returns the lead to that state.
The Slack alert's Log Outcome menu offers all six outcomes in one tap — Qualified (SQL), Rejected (Not ICP), Rejected (No Response), Nurture, Closed Won, and Closed Lost — the same set available from the lead detail page and this API.
The Optional Comment
Every disposition can carry a free-text comment (up to 2000 characters) — for example, "Booked a demo for Thursday" or "Competitor scraping our pricing." Comments are shown to your marketing operations team in the Sales feedback card and in the Recent Rep Notes list. They help reps explain their reasoning.
Comments are never used in scoring math. They are read by people, not by the calibration engine.
What Gets Recorded Automatically
When a disposition is set, kenbun stamps the lead with two values, captured automatically at the moment the disposition is set:
- Score — the lead's engagement score at that moment.
- Scoring rules version — which version of your scoring rules produced that score.
Together these form a (score, rules version, outcome) record that the calibration engine can learn from. The stamp is taken at the moment the rep records the disposition, not at the time the alert was sent — so it reflects the lead's state when the decision was actually made.
A few notes on the stamp:
- If a lead has no active primary scoring ruleset, the score and rules version are left empty. The disposition is still recorded.
- The stamp is captured once, when an outcome is first chosen. Changing the comment later does not re-stamp it; choosing a different outcome does.
Sample Leads Cannot Be Dispositioned
If your workspace contains demo "sample" leads (created by the Sample Data generator), those leads cannot be dispositioned. Attempting to do so returns a clear error, and the lead is left untouched. This keeps synthetic demo data out of your real sales-feedback history.
PUT /leads/{leadID}/disposition
Set or update the disposition on a single lead.
When to Use This
- Log a sales outcome: A rep marks a lead as Qualified or Not ICP after working it.
- Add or update a comment: Attach the rep's reasoning to the outcome.
- Correct a mistake: Change a lead from one outcome to another.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
leadID | string (UUID) | Yes | Lead identifier (path parameter) |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
disposition | string | Yes | One of the six values in the table above |
comment | string | No | Optional rep note, up to 2000 characters |
How the comment field behaves:
- Omitted — the comment is preserved if you re-save the same outcome; it is cleared if you change to a different outcome.
- Empty or whitespace — the comment is cleared.
- Provided — the comment is set (leading and trailing spaces are trimmed).
{
"disposition": "accepted_sql",
"comment": "Booked a demo"
}
Response
200 OK
{
"status": "ok",
"disposition": {
"disposition": "accepted_sql",
"comment": "Booked a demo",
"set_at": "2026-06-10T14:03:12Z",
"set_by_label": "alice@acme.com",
"score": 74,
"ruleset_version_id": "8b2e0c1a-3d4f-4a5b-9c6d-7e8f9a0b1c2d"
},
"previous": {
"disposition": "rejected_not_icp",
"set_at": "2026-06-09T11:20:00Z",
"set_by_label": "bob@acme.com",
"comment": "competitor scraping pricing",
"score": 61,
"ruleset_version_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
},
"restamped": true
}
| Field | Type | Description |
|---|---|---|
status | string | Always "ok" on success |
disposition.disposition | string | The outcome now recorded on the lead |
disposition.comment | string or null | The current comment, or null if none |
disposition.set_at | timestamp | When this outcome was first chosen |
disposition.set_by_label | string or null | Who set the outcome (for example, an email address) |
disposition.score | integer or null | Lead score stamped at set time, or null |
disposition.ruleset_version_id | string (UUID) or null | Scoring rules version stamped at set time, or null |
previous | object or null | The outcome this one replaced, or null if the lead had none. Same fields as disposition, plus comment. |
restamped | boolean | true when the outcome value actually changed (and was re-stamped); false for a same-outcome re-save |
Example
curl -X PUT "https://api.kenbun.io/leads/3d4f4a5b-9c6d-7e8f-9a0b-1c2d3e4f5a6b/disposition" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"disposition": "accepted_sql", "comment": "Booked a demo"}'
Common Errors
| Status | Error | Meaning | Solution |
|---|---|---|---|
| 400 | invalid_disposition | The disposition value is not one of the six allowed values | Send one of the values listed above; the response includes the full allowed list |
| 400 | comment_too_long | The comment exceeds 2000 characters | Shorten the comment to 2000 characters or fewer |
| 404 | lead_not_found_or_merged | The lead does not exist in your active Organizational Unit, or it was merged into another lead | Verify the lead ID; merged leads are dispositioned on the surviving lead |
| 409 | sample_lead | The lead is a demo sample lead | Sample leads cannot be dispositioned |
An invalid disposition returns the allowed list so you can correct the request:
{
"error": "invalid_disposition",
"allowed": ["accepted_sql", "rejected_not_icp", "rejected_no_response", "nurture", "closed_won", "closed_lost"]
}
DELETE /leads/{leadID}/disposition
Clear the disposition on a lead, returning it to the "no disposition" state. The response includes the outcome that was removed, so your app can confirm what was cleared.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
leadID | string (UUID) | Yes | Lead identifier (path parameter) |
Response
200 OK — when a disposition was cleared:
{
"status": "ok",
"previous": {
"disposition": "accepted_sql",
"set_at": "2026-06-10T14:03:12Z",
"set_by_label": "bob@acme.com",
"comment": "Booked a demo",
"score": 74,
"ruleset_version_id": "8b2e0c1a-3d4f-4a5b-9c6d-7e8f9a0b1c2d"
}
}
200 OK — when the lead had no disposition (nothing to clear):
{
"status": "ok",
"previous": null
}
| Field | Type | Description |
|---|---|---|
status | string | Always "ok" on success |
previous | object or null | The outcome that was cleared, or null if there was nothing to clear |
Example
curl -X DELETE "https://api.kenbun.io/leads/3d4f4a5b-9c6d-7e8f-9a0b-1c2d3e4f5a6b/disposition" \
-H "Authorization: Bearer <token>"
Common Errors
| Status | Error | Meaning | Solution |
|---|---|---|---|
| 404 | lead_not_found_or_merged | The lead does not exist in your active Organizational Unit, or it was merged | Verify the lead ID |
| 409 | sample_lead | The lead is a demo sample lead | Sample leads cannot be dispositioned |
GET /leads/disposition-stats
Summarize disposition activity across your active Organizational Unit. This powers the Sales feedback card your marketing operations team sees, including how many outcomes were logged, how concentrated they are among reps, the average score per outcome under your current rules, and the most recent rep notes.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
since_days | integer | No | Look-back window in days. Default 90. Clamped to the range 1–365. |
Response
200 OK
{
"since_days": 90,
"total": 18,
"distinct_actors": 5,
"top_actor_share": 0.44,
"current_ruleset_version_id": "8b2e0c1a-3d4f-4a5b-9c6d-7e8f9a0b1c2d",
"by_disposition": [
{
"disposition": "accepted_sql",
"count": 9,
"count_current_version": 7,
"avg_score_current_version": 74.2
}
],
"recent_comments": [
{
"lead_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"lead_label": "John Doe",
"disposition": "rejected_not_icp",
"set_by_label": "rep@acme.com",
"set_at": "2026-06-09T11:20:00Z",
"comment": "competitor scraping pricing",
"comment_truncated": false
}
]
}
| Field | Type | Description |
|---|---|---|
since_days | integer | The look-back window applied to this response |
total | integer | Total dispositions set within the window |
distinct_actors | integer | How many distinct people set dispositions |
top_actor_share | number or null | The fraction of dispositions set by the single most active person (for example, 0.44 means 44%) |
current_ruleset_version_id | string (UUID) or null | The scoring rules version currently in effect. The per-outcome average scores are restricted to dispositions stamped with this version. null when there is no active primary ruleset. |
by_disposition | array | One entry per outcome present in the window (always an array) |
by_disposition[].disposition | string | The outcome value |
by_disposition[].count | integer | Total dispositions of this outcome in the window |
by_disposition[].count_current_version | integer | How many of those were stamped with the current rules version |
by_disposition[].avg_score_current_version | number or null | Average stamped score among current-version dispositions, or null |
recent_comments | array | Up to 10 most recent rep notes, newest first (always an array) |
recent_comments[].lead_id | string (UUID) | The lead the note belongs to |
recent_comments[].lead_label | string | A friendly display name for the lead |
recent_comments[].disposition | string | The outcome recorded with the note |
recent_comments[].set_by_label | string or null | Who set the disposition |
recent_comments[].set_at | timestamp | When the disposition was set |
recent_comments[].comment | string | The note text (long notes are shortened to a preview) |
recent_comments[].comment_truncated | boolean | true when the note was shortened; open the lead to read the full note |
Example
curl -X GET "https://api.kenbun.io/leads/disposition-stats?since_days=90" \
-H "Authorization: Bearer <token>"
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 400 | since_days is not a valid number | Send a whole number (it is clamped to 1–365) |
| 401 | Unauthorized | Check authentication credentials |
GET /leads/disposition-quality
Compare the score kenbun gave each lead against the outcome sales actually logged. This powers the Score vs. Outcome view on the calibration review page, and is useful any time you want to find where scoring and sales disagreed.
The report has four parts:
- False positives — leads that scored high yet were rejected.
- False negatives — leads that scored low yet were qualified or won.
- Distribution — the spread of stamped scores per outcome.
- Time to disposition — how long leads waited between owner assignment and an outcome.
Every figure uses the score stamped at the moment each outcome was logged. Merged-away leads and sample (demo) leads are excluded.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
since_days | integer | No | Look-back window in days. Default 90. Clamped to 1–365. |
high_score | integer | No | The score at or above which a rejected lead counts as a false positive. When omitted, derived from your highest engagement level threshold, falling back to 75. |
low_score | integer | No | The score below which a qualified or won lead counts as a false negative. Default 40. |
limit | integer | No | Maximum leads in each of the false-positive and false-negative lists. Default 25. Clamped to 100. |
Response
200 OK
{
"since_days": 90,
"high_score": 75,
"low_score": 40,
"current_ruleset_version_id": "8b2e0c1a-3d4f-4a5b-9c6d-7e8f9a0b1c2d",
"false_positives": [
{
"lead_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"lead_label": "Jane Doe",
"disposition": "rejected_not_icp",
"disposition_score": 82,
"set_by_label": "rep@acme.com",
"comment_excerpt": "Student researching, not a buyer",
"disposition_set_at": "2026-06-09T11:20:00Z"
}
],
"false_negatives": [
{
"lead_id": "9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
"lead_label": "john@bigco.com",
"disposition": "closed_won",
"disposition_score": 31,
"set_by_label": "rep@acme.com",
"comment_excerpt": "Inbound referral, fast close",
"disposition_set_at": "2026-06-08T09:00:00Z"
}
],
"distribution": [
{
"disposition": "accepted_sql",
"count": 12,
"min": 41,
"q1": 58,
"median": 67,
"q3": 74,
"max": 88
}
],
"time_to_disposition": [
{
"disposition": "accepted_sql",
"count": 12,
"avg_days": 4.2,
"median_days": 3.0
}
]
}
| Field | Type | Description |
|---|---|---|
since_days | integer | The look-back window applied to this response |
high_score | integer | The false-positive score floor applied |
low_score | integer | The false-negative score ceiling applied |
current_ruleset_version_id | string (UUID) or null | The scoring rules version the distribution is restricted to. null when there is no active primary ruleset. |
false_positives | array | High-scored leads that were rejected (always an array) |
false_negatives | array | Low-scored leads that were qualified or won (always an array) |
false_positives[].disposition_score | integer or null | Score stamped when the outcome was logged |
false_positives[].set_by_label | string or null | Who logged the outcome |
false_positives[].comment_excerpt | string or null | First 150 characters of the rep's note, if any |
distribution | array | Per-outcome score spread: min, q1, median, q3, max, and count (always an array) |
time_to_disposition | array | Per-outcome avg_days and median_days from owner assignment to outcome (always an array) |
Example
curl -X GET "https://api.kenbun.io/leads/disposition-quality?since_days=90" \
-H "Authorization: Bearer <token>"
CSV Export
GET /leads/disposition-quality/export.csv returns the false-positive and false-negative leads as a single CSV. A section column labels each row as false_positive or false_negative. It accepts the same query parameters as the JSON endpoint.
curl -X GET "https://api.kenbun.io/leads/disposition-quality/export.csv" \
-H "Authorization: Bearer <token>" -o disposition_quality.csv
Filtering Leads by Disposition
The lead list and its CSV export accept a disposition query parameter so you can pull just the leads with a given outcome. Pass a comma-separated list of one or more of the six values, plus the sentinel none for leads with no disposition set:
# Leads marked Qualified or Nurture
curl -X GET "https://api.kenbun.io/leads?disposition=accepted_sql,nurture" \
-H "Authorization: Bearer <token>"
# Leads with no disposition yet
curl -X GET "https://api.kenbun.io/leads?disposition=none" \
-H "Authorization: Bearer <token>"
The same filter is available as checkboxes under Analyze > Leads and carries through to the on-screen CSV export. See Leads for the full parameter list.
How Dispositions Feed Calibration
Once your team has logged enough outcomes, kenbun can suggest improved scoring weights based on which scores actually converted. To enable this:
- Go to Configure > Scoring and set your conversion definition to Sales Disposition. This tells kenbun to learn from outcomes marked Qualified (SQL) or Closed Won.
- Keep logging dispositions as reps work leads. Calibration unlocks once at least 30 qualifying dispositions have been recorded on scored leads within your look-back window.
- When suggestions are ready, review them under Configure > Scoring and apply the ones you agree with. Nothing changes automatically.
Until 30 qualifying dispositions exist, the calibration panel shows your progress (for example, "Only 12 of 30 sales dispositions so far") rather than an error. This same minimum applies whether your conversion definition is Sales Disposition or Manually Labeled outcomes.
Notes
- All disposition data is scoped to your active Organizational Unit, set automatically by your account context.
- Setting or clearing a disposition is recorded in your audit log, including the comment and the previous outcome it replaced.
- Merged leads are dispositioned on the surviving lead; the merged-away lead returns a "not found or merged" error.
- Re-confirming a disposition that is already set does not change its timestamp and does not affect calibration counts.
Related Endpoints
- Lead Owner Assignment — assign sales reps to leads
- Leads — list leads with their current details (supports the
dispositionfilter) - Lead Detail — view an individual lead
Related Guides
- Calibration Readiness — track progress toward the 30-outcome floor
- Score vs. Outcome — see where scoring agreed with sales and where it missed
- Slack: Disposition Nudges — Slack reminders and payoff messages
- HubSpot: Disposition Writeback — sync outcomes to your CRM