Synchronous Scoring API
Score a lead, a profile, or a company the moment you have the data — and get the answer back in the same response.
Most automations want an immediate, trustworthy answer to one question: "Is this signal worth acting on right now?" The Synchronous Scoring endpoint answers that question in a single call. You send a signal (or a profile, or a company), and kenbun returns the score, the level, and the exact rules that drove it — all in the response body, with no waiting and no polling.
The same call can also build your lead list as it scores. When you include profile details, kenbun saves them onto the lead. When you include company details, kenbun finds or creates the matching company (account) and links the lead to it. So a single request can create the lead, enrich it, attach its company, and score all of it at once.
This is the ideal endpoint for outbound and enrichment workflows built in tools like n8n, Zapier, or your own backend, where you want to branch on a score instantly: route hot signals to a rep, drop cold ones, and suppress anyone who is disqualified.
Why synchronous scoring matters
The standard Ingest endpoint is built for high-volume, fire-and-forget event capture: it accepts an event and scores it shortly after. That is perfect for website beacons and bulk activity, but it is not designed for "score this, then tell me the answer so I can decide what to do next."
Synchronous Scoring is built for exactly that decision point:
- Immediate answer. The score, level, and contributing rules come back in the response — no second request to look it up.
- Honest results. A genuine score of zero returns success. A disqualified
lead returns success with a clear
disqualifiedflag and reason. Only real failures return an error status, so your automation can always tell the difference between "scored zero" and "could not score." - Multi-dimension. Score an engagement signal, a firmographic profile, and a company in one call — or just the one you care about. Only the dimensions you send are scored and returned.
- Repeatable. The same input always produces the same result, so you can test, replay, and trust it in production.
POST /scoring/score
Score one or more dimensions for a lead, identified by an external id.
Send at least one of event, profile, or account. The lead is found by its
external_id (for example, a LinkedIn profile reference or a company domain), or
created automatically if it does not exist yet.
This call can also save the data you send:
- Sending a
profilescores the firmographic fit and saves those details onto the lead. - Sending an
accountscores the company fit and finds or creates the matching company, then links the lead to it.
What your token is allowed to do depends on which of these you send. See Permissions your token needs below.
Authentication
This endpoint uses a Personal Access Token. Send it as a bearer token:
Authorization: Bearer <your-personal-access-token>
Your organization and active Organizational Unit are set automatically by your account context — you never send them in the request body.
Permissions Your Token Needs
The permissions a token needs depend on what you ask it to do. Scoring alone needs only the scoring permission. Saving the lead or company needs the matching write permission too.
| What you send | What kenbun does | Token permissions required |
|---|---|---|
event only | Scores the engagement signal | write:scoring |
profile | Scores fit and saves the details onto the lead | write:scoring + write:leads |
account | Scores fit and finds/creates the company and links it | write:scoring + write:accounts |
| All three | Creates, enriches, links, and scores everything | write:scoring + write:leads + write:accounts |
If a token is missing a required permission, the request returns a 403 error
with the reason. For example, sending a profile with a scoring-only token
returns 403 because saving the lead requires write:leads.
Tip: Give a score-only automation a token with just write:scoring. Give a
create-score-enrich automation a token with all three permissions.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
external_id | string | Yes | The identifier used to find or create the lead (for example, linkedin:12345 or acme.com). |
idempotency_key | string | No | A key that makes retries safe. Resending the same key for the same lead returns the original result. The key is automatically tied to the lead's external_id, so it is safe to reuse one value across different leads (for example an automation run id) without one lead overwriting another. If omitted, kenbun derives a safe per-lead key for you (the simplest, recommended option). |
triggers_enabled | boolean | No | When true, downstream notifications and automations may fire for the engagement signal. Defaults to false. |
source | string | No | A label for where the signal came from (for example, n8n). Defaults to score. The reserved values test and sample_data are not allowed. |
rulesets | object | No | Optional names of the scoring sets to use per dimension. Omit a dimension to use its primary set. |
event | object | No | An engagement signal to score (engagement dimension). |
profile | object | No | Firmographic attributes to score (profile fit dimension). |
account | object | No | Company attributes to score (account fit dimension). |
At least one of event, profile, or account must be present.
The event object describes a single engagement signal. kenbun events are
channel-agnostic: the event itself only carries what happened, when it
happened, and a free-form bag of details. There is no separate channel, text, or
link field on the event — those go inside metadata.
| Field | Type | Description |
|---|---|---|
event_type | string | The kind of thing that happened (for example, hiring_post). This is what happened, not a channel. It must match a configured engagement rule's event type exactly. If it does not match any rule, the engagement score is simply 0; that is a normal 200 response, not an error. |
occurred_at | string | When it happened, in standard date-time format. Defaults to now. |
metadata | object | A free-form set of details about the signal. Put the marketing channel (for example, metadata.channel: "linkedin"), the source URL, the post text, and any custom attributes here. This is what your attribute-based engagement rules match against. |
The marketing channel, if you want to record it, belongs in metadata.channel
(for example, "linkedin"). Keep event_type for the kind of event only.
Whenever you send an event, kenbun records it on the lead's timeline — even if
its event_type matches no rule (engagement score 0) and even if no engagement
scoring is configured yet. The event is always captured so the activity history
stays complete.
The profile object holds firmographic attributes about the person, such as
job_title, seniority, industry, and company_size. Send whatever your
profile rules look at, using the same property names your rules use. When you
include a profile, kenbun also saves these details onto the lead (this is why a
profile needs the write:leads permission).
Use the property names your profile rules reference. kenbun treats a few common
synonyms as equivalent, so title and job_title (and company /
company_name) are interchangeable — a payload that sends title will satisfy a
rule that looks at job_title, and vice-versa. For any other custom attribute,
send the exact key your rule uses.
The account object holds company attributes, such as domain, industry,
employees, and revenue. Send whatever your account rules look at. When you
include an account, kenbun finds or creates the matching company and links the
lead to it, then saves these details onto that company (this is why an account
needs the write:accounts permission).
kenbun matches the company in this order, using the first value you provide:
- CRM company id (
external_id) — the company's id in your connected CRM, if you send one. - Website domain (
domain) — used when no CRM id is sent. - Company name (
name) — used when neither a CRM id nor a domain is sent.
If no existing company matches, kenbun creates a new one. Sending the CRM id (or a consistent domain) keeps the same company from being created twice.
{
"external_id": "linkedin:12345",
"idempotency_key": "n8n-exec-abc",
"source": "n8n",
"rulesets": {
"engagement": "outbound-intent",
"profile": "icp-fit"
},
"event": {
"event_type": "hiring_post",
"occurred_at": "2026-06-06T12:00:00Z",
"metadata": {
"channel": "linkedin",
"text": "We are hiring 5 SDRs"
}
},
"profile": {
"job_title": "VP Sales",
"seniority": "VP",
"industry": "SaaS",
"company_size": "500"
}
}
Response
200 OK — the signal was scored. Note that a score of zero and a disqualified
lead both return 200; only real failures return an error.
| Field | Type | Description |
|---|---|---|
external_id | string | The identifier you sent. |
lead_id | string | The lead that was found or created. |
account_id | string or null | The company (account) that was found or created and linked to the lead. Present when you sent an account; otherwise null. |
idempotency_key | string | The key used for this request. |
scored_at | string | When the score was computed. |
disqualified | boolean | true if the lead matched a hard-disqualification rule. |
disqualified_reason | string or null | The reason and dimension when disqualified; otherwise null. |
dimensions_scored | array | Which dimensions were scored (engagement, profile, account). |
enriched | object | Which records were saved by this request, for example {"lead": true, "account": true}. lead is true when a profile was saved onto the lead; account is true when a company was found or created and linked. |
cached | boolean | true if this result was replayed from a previous identical request. |
engagement | object | Present only if you sent an event. |
profile | object | Present only if you sent a profile. |
account | object | Present only if you sent an account. |
Each dimension result includes a score, a level label, the ruleset name
used, and a list of drivers — the individual rules that contributed to the
score, so the number is always explainable.
{
"external_id": "linkedin:12345",
"lead_id": "0190d8e2-5a6b-7c3d-9e2f-1a2b3c4d5e6f",
"account_id": null,
"idempotency_key": "n8n-exec-abc",
"scored_at": "2026-06-06T12:00:01Z",
"disqualified": false,
"disqualified_reason": null,
"dimensions_scored": ["engagement", "profile"],
"enriched": { "lead": true, "account": false },
"cached": false,
"engagement": {
"score": 25,
"level": "warm",
"ruleset": "outbound-intent",
"surge_boost": 0,
"decay_applied": 0,
"drivers": [
{
"rule_id": "0190d8e2-1111-7c3d-9e2f-aaaaaaaaaaaa",
"event_type": "hiring_post",
"label": "event_type equals hiring_post",
"weight": 25,
"total": 25
}
]
},
"profile": {
"score": 40,
"level": "strong-fit",
"ruleset": "icp-fit",
"drivers": [
{
"rule_id": "0190d8e2-2222-7c3d-9e2f-bbbbbbbbbbbb",
"property": "job_title",
"condition": "equals",
"value": "VP Sales",
"weight": 40,
"matched": true
}
]
}
}
Examples
Score a single engagement signal:
curl -X POST "https://api.kenbun.io/scoring/score" \
-H "Authorization: Bearer <your-personal-access-token>" \
-H "Content-Type: application/json" \
-d '{
"external_id": "linkedin:12345",
"source": "n8n",
"event": {
"event_type": "hiring_post",
"occurred_at": "2026-06-06T12:00:00Z",
"metadata": { "channel": "linkedin" }
}
}'
Score company fit only:
curl -X POST "https://api.kenbun.io/scoring/score" \
-H "Authorization: Bearer <your-personal-access-token>" \
-H "Content-Type: application/json" \
-d '{
"external_id": "acme.com",
"account": {
"domain": "acme.com",
"industry": "SaaS",
"employees": 1200
}
}'
Create, enrich, and score in one call:
This sends a profile and a company, so kenbun saves the profile onto the lead,
finds or creates the company, links them, and scores all three dimensions. The
token needs write:scoring, write:leads, and write:accounts.
curl -X POST "https://api.kenbun.io/scoring/score" \
-H "Authorization: Bearer <your-personal-access-token>" \
-H "Content-Type: application/json" \
-d '{
"external_id": "linkedin:12345",
"source": "n8n",
"profile": { "role": "VP Sales", "icp_tier": "A" },
"account": {
"external_id": "hubspot:7788",
"domain": "acme.com",
"name": "Acme Inc",
"employees": 1200
}
}'
The response includes the linked account_id and an enriched object showing
what was saved, for example "enriched": { "lead": true, "account": true }.
Safely retry with an idempotency key:
curl -X POST "https://api.kenbun.io/scoring/score" \
-H "Authorization: Bearer <your-personal-access-token>" \
-H "Content-Type: application/json" \
-d '{
"external_id": "linkedin:12345",
"idempotency_key": "n8n-exec-abc",
"profile": { "role": "VP Sales", "icp_tier": "A" }
}'
Sending the same idempotency_key again for the same lead returns the
original result with cached: true, so a retried workflow never double-counts.
The key is scoped to each lead's external_id, so reusing one value across many
leads (such as a single automation run id) is safe — each lead is still created
and scored. The simplest approach is to omit idempotency_key entirely and let
kenbun derive a safe per-lead key.
Common Errors
Errors are honest: a real score (including zero) and a disqualified lead return
200. The statuses below mean something actually went wrong.
| Status | Meaning | Solution |
|---|---|---|
| 400 | Invalid request | Provide external_id and at least one of event, profile, or account. Check your date-time format and avoid the reserved source values test and sample_data. |
| 401 | Not authenticated | Send a valid Personal Access Token. |
| 403 | Missing permission | Your token needs the right permissions for what you sent: write:scoring to score, plus write:leads when you send a profile and write:accounts when you send an account. See Permissions your token needs. |
| 404 | Ruleset not found | A named ruleset in rulesets does not exist or is not enabled for that dimension. Use a valid name or omit it to use the primary set. |
| 429 | Usage limit reached | You have reached your plan's monthly scoring limit. |
| 500 | Scoring failed | A genuine internal error (not the same as a score of zero). Retry, and contact support if it persists. |
Notes
- Each dimension's score reflects the signal you submit under its scoring set — it is deterministic, so the same input always produces the same result.
- Saving the lead and company is additive: kenbun merges the details you send
into what is already stored, rather than replacing the whole record. The
enrichedobject in the response tells you exactly what was saved. - The
surge_boostanddecay_appliedfields on the engagement result are always0here: this endpoint scores a single fresh signal on its own, and surge and decay are based on a lead's history over time. - Scoring activity through this endpoint counts toward your plan's usage. Sample and test data are always excluded from usage.
- The organization and Organizational Unit are determined from your account
context — do not send
unit_idin the request body.
Related Endpoints
- Ingest - Capture high-volume engagement events for asynchronous scoring
- Lead Score - Read a lead's current stored score
- Score Explanation - Explain a lead's stored score in detail
- Disqualification - Manage hard-disqualification rules