Skip to main content

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 disqualified flag 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 profile scores the firmographic fit and saves those details onto the lead.
  • Sending an account scores 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 sendWhat kenbun doesToken permissions required
event onlyScores the engagement signalwrite:scoring
profileScores fit and saves the details onto the leadwrite:scoring + write:leads
accountScores fit and finds/creates the company and links itwrite:scoring + write:accounts
All threeCreates, enriches, links, and scores everythingwrite: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

FieldTypeRequiredDescription
external_idstringYesThe identifier used to find or create the lead (for example, linkedin:12345 or acme.com).
idempotency_keystringNoA 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_enabledbooleanNoWhen true, downstream notifications and automations may fire for the engagement signal. Defaults to false.
sourcestringNoA label for where the signal came from (for example, n8n). Defaults to score. The reserved values test and sample_data are not allowed.
rulesetsobjectNoOptional names of the scoring sets to use per dimension. Omit a dimension to use its primary set.
eventobjectNoAn engagement signal to score (engagement dimension).
profileobjectNoFirmographic attributes to score (profile fit dimension).
accountobjectNoCompany 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.

FieldTypeDescription
event_typestringThe 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_atstringWhen it happened, in standard date-time format. Defaults to now.
metadataobjectA 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:

  1. CRM company id (external_id) — the company's id in your connected CRM, if you send one.
  2. Website domain (domain) — used when no CRM id is sent.
  3. 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.

FieldTypeDescription
external_idstringThe identifier you sent.
lead_idstringThe lead that was found or created.
account_idstring or nullThe company (account) that was found or created and linked to the lead. Present when you sent an account; otherwise null.
idempotency_keystringThe key used for this request.
scored_atstringWhen the score was computed.
disqualifiedbooleantrue if the lead matched a hard-disqualification rule.
disqualified_reasonstring or nullThe reason and dimension when disqualified; otherwise null.
dimensions_scoredarrayWhich dimensions were scored (engagement, profile, account).
enrichedobjectWhich 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.
cachedbooleantrue if this result was replayed from a previous identical request.
engagementobjectPresent only if you sent an event.
profileobjectPresent only if you sent a profile.
accountobjectPresent 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.

StatusMeaningSolution
400Invalid requestProvide 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.
401Not authenticatedSend a valid Personal Access Token.
403Missing permissionYour 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.
404Ruleset not foundA 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.
429Usage limit reachedYou have reached your plan's monthly scoring limit.
500Scoring failedA 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 enriched object in the response tells you exactly what was saved.
  • The surge_boost and decay_applied fields on the engagement result are always 0 here: 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_id in the request body.