Download OpenAPI specification:
kenbun API for event ingestion, scoring, segments, triggers, and organization unit (OU) workflows. Notes on scoping and security:
# HELP events_ingested_total Total number of events ingested (accepted). # TYPE events_ingested_total counter events_ingested_total 0 # HELP queue_depth Current depth of the async processing queue. # TYPE queue_depth gauge queue_depth 0 # HELP event_to_slack_latency_seconds Latency in seconds from event ingestion to Slack notification. # TYPE event_to_slack_latency_seconds histogram # HELP http_requests_total Total number of HTTP requests by status code and method. # TYPE http_requests_total counter
Returns the authenticated user's basic profile. Requires a valid JWT.
{- "org_member_id": "om_abc123",
- "first_name": "Jane",
- "last_name": "Doe",
- "email": "jane@example.com",
- "role": "admin",
- "verified": true
}Requires authentication and an active OU selected via POST /org-units/active. The server derives org_id and unit_id from the authenticated context; clients must not send unit_id.
Identify the target lead with EITHER:
lead_id, ORaliases array (preferred). Each entry supplies alias_kind and raw_value.Backward compatibility: top‑level alias_kind and alias are accepted on ingest but are deprecated and not returned by read endpoints.
| lead_id | string Existing lead identifier; optional when aliases are provided |
| alias_kind | string Enum: "email" "phone" "external_id" "cookie" "social" Deprecated. Use |
| alias | string Deprecated. Use |
Array of objects Preferred. One or more aliases to identify or create a lead. | |
| event_type required | string |
| timestamp | string <date-time> |
object Arbitrary JSON metadata. Max 100KB; larger payloads are rejected with 413. For DECAY events, the server sets metadata.channel to 'system'. |
{- "lead_id": "ld_123",
- "event_type": "Page View",
- "timestamp": "2025-08-15T12:00:00Z",
- "metadata": {
- "path": "/pricing",
- "utm_source": "newsletter"
}
}{- "message": "event ingested",
- "metadata": {
- "geo_country": "US",
- "geo_region": "California",
- "geo_city": "San Francisco",
- "user_agent": "Mozilla/5.0"
}
}Manual fallback for logging a LinkedIn connection request or message. Constructs the same behavioral event as the extension auto-capture path (source='linkedin_extension_manual', alias_kind='social') and funnels it through scoring + the HubSpot fan-out.
Requires authentication and an active OU. Org and OU are derived from the authenticated context; clients must not send unit_id.
linkedin_url is canonicalized server-side (lowercase, no scheme/www/trailing slash) so duplicate URLs with different casing resolve to the same lead.
log_to_hubspot defaults to true. Set false to score in kenbun only.
| activity_type required | string Enum: "connection_request_sent" "message_sent" Type of LinkedIn outbound touch. |
| linkedin_url required | string Raw or canonical LinkedIn profile URL (e.g. https://linkedin.com/in/jane-doe). |
| recipient_name | string Display name of the recipient (for HubSpot note body). |
| body | string Message body (included in the HubSpot note only when provided; consent-gated by the caller). |
| idempotency_key | string Stable key to prevent double-writes across retries. Generated server-side when absent. |
| log_to_hubspot | boolean Default: true Fan-out the touch to HubSpot as a Note engagement. Set false to score in kenbun only. |
{- "activity_type": "connection_request_sent",
- "recipient_name": "Jane Doe",
- "idempotency_key": "linkedin.com/in/jane-doe|2026-06-03T14:00:00Z"
}{- "message": "activity logged",
- "lead_id": "550e8400-e29b-41d4-a716-446655440000",
- "event_type": "LinkedIn Connection Request Sent"
}Accepts a CSV upload. The server enqueues each row as an EngagementEvent under the authenticated org and active OU. Required columns: event_type. Optional: lead_id, timestamp (RFC3339), metadata (JSON), alias columns (external_id,email,phone,cookie,social).
| file | string <binary> CSV file with header row |
{- "message": "string"
}Returns events limited to the active Organization Unit (OU).
| page | integer >= 1 Default: 1 |
| limit | integer [ 1 .. 500 ] Default: 50 |
| bot | string Enum: "hide" "only" Likely-bot view filter. |
| metadata | Array of strings Repeatable metadata key/value filters (e.g., metadata=utm_campaign:fall2025) |
| logic | string Default: "and" Enum: "and" "or" Combine metadata filters with AND (default) or OR |
[- {
- "id": "ev_001",
- "org_id": "org_1",
- "unit_id": "unit_default",
- "lead_id": "ld_123",
- "event_type": "Page View",
- "timestamp": "2025-08-15T12:00:00Z",
- "metadata": {
- "path": "/pricing"
}
}
]| leadID required | string |
[- {
- "id": "ev_002",
- "org_id": "org_1",
- "unit_id": "unit_default",
- "lead_id": "ld_123",
- "event_type": "signup",
- "timestamp": "2025-08-15T12:05:00Z"
}
]Returns per-ruleset score deltas and before/after totals for a single event.
| eventID required | string |
{- "event_id": "string",
- "lead_id": "string",
- "event_type": "string",
- "occurred_at": "2019-08-24T14:15:22Z",
- "rulesets": [
- {
- "ruleset_id": "string",
- "ruleset_name": "string",
- "is_primary": true,
- "score_delta": 0,
- "score_before": 0,
- "score_after": 0,
- "rule_weight": 0
}
]
}Returns a composite view of lead activity: filtered engagement events, orders, and a unified timeline.
Unified Activity Stream (ADR-0010):
engagement_events tableorders table (represented as synthetic activity projections)OU Scoping: Only returns data for the caller's active Organizational Unit.
Synthetic Order Projections:
orders array and timeline with type: "order"engagement_events tableFiltering behavior:
range, start, end: Apply to both events and orderschannel, minScore, maxLatency, hasDelta: Apply to events only| leadID required | string Lead UUID |
| range | string Default: "last_7d" Enum: "all" "last_7d" "last_24h" "last_30d" "last_90d" "today" "yesterday" "custom" Time range filter (applies to both events and orders) |
| start | string <date-time> Start timestamp (RFC3339, UTC). Required when range=custom |
| end | string <date-time> End timestamp (RFC3339, UTC). Required when range=custom |
| channel | string Enum: "all" "web" "email" "slack" "webinar" "crm" "system" Channel filter (applies to events only) |
| minScore | integer >= 0 Minimum score filter (applies to events only) |
| maxLatency | integer >= 1 Maximum latency in ms (applies to events only) |
| hasDelta | string Enum: "1" "true" "0" "false" Show only events with score deltas (applies to events only) |
{- "events": [
- {
- "id": "evt-123",
- "event_type": "Page View",
- "timestamp": "2025-11-15T09:00:00Z",
- "metadata": {
- "url": "/products/widget"
}, - "source": "web"
}
], - "orders": [
- {
- "id": "order-456",
- "type": "order",
- "timestamp": "2025-11-15T09:10:00Z",
- "order_id": "456",
- "total_amount": 129.99,
- "status": "completed",
- "items_summary": "2 items"
}
], - "timeline": [
- {
- "id": "evt-123",
- "ts": "2025-11-15T09:00:00Z",
- "title": "Page View",
- "type": "event",
- "subtext": "/products/widget",
- "jumpTo": "evt-123",
- "dotColor": "brand",
- "channel": "web"
}, - {
- "id": "order-456",
- "ts": "2025-11-15T09:10:00Z",
- "title": "Order Placed",
- "type": "order",
- "subtext": "$129.99 • 2 items • completed",
- "jumpTo": "order-456",
- "dotColor": "green",
- "channel": "order"
}
]
}Server-aggregated metrics for the Overview dashboard: event/lead counts with prior-period deltas, the engagement x profile distribution matrix, the 7-day activity heatmap, deals summary, and the single hottest lead (hot_lead) at or above the hot threshold for the "Act now" banner. hot_lead is null when no lead currently clears the threshold.
| days | integer Default: 7 Length of the current period in days (prior period is the same length immediately before it). |
{- "events_count": 220,
- "active_leads_count": 84,
- "hot_leads_count": 1,
- "new_leads_count": 82,
- "hot_threshold": 120,
- "deals_enabled": false,
- "hot_lead": {
- "lead_id": "9f1c...e2",
- "display_name": "balwill0503@gmail.com",
- "score": 630,
- "account_name": "salesforce",
- "last_seen": "2026-05-30T09:42:00Z",
- "top_page": "/pricing/",
- "session_count": 4
}
}Returns aggregated engagement scoring patterns across all closed-won deals in the active OU. Includes average lead scores at MQL, SQL, Opportunity, and Close milestones, and a ranked list of the event types most associated with closed pipeline. Use this to calibrate scoring thresholds and validate rule weights against real conversion data.
{- "closed_won_deals": 42,
- "avg_score_at_mql": 28.5,
- "avg_score_at_sql": 51.2,
- "avg_score_at_opportunity": 68,
- "avg_score_at_close": 84.7,
- "top_event_types": [
- {
- "event_type": "demo_request",
- "deal_count": 38,
- "total_points": 4200
}, - {
- "event_type": "page_view",
- "deal_count": 42,
- "total_points": 3150
}, - {
- "event_type": "email_open",
- "deal_count": 31,
- "total_points": 1900
}
]
}Returns campaign performance aggregated from UTM parameters on engagement events in the
active Organizational Unit. Each row represents a unique value of utm_campaign,
utm_source, or utm_medium depending on group_by. Pipeline metrics are computed via
the lead → account → deal join and are multi-touch and unweighted; the same deal counts
in full toward every campaign that touched the account.
| group_by | string Default: "campaign" Enum: "campaign" "source" "medium" Which UTM key to aggregate on. Defaults to |
| start_date | string Lower bound for event |
| end_date | string Upper bound for event |
{- "group_by": "campaign",
- "campaigns": [
- {
- "name": "spring-2026-launch",
- "total_events": 1284,
- "unique_leads": 312,
- "mql_count": 47,
- "total_points": 6420,
- "deals_count": 12,
- "pipeline_value": 485000,
- "closed_won_value": 120000,
- "first_event_at": "2026-03-01T08:14:22Z",
- "last_event_at": "2026-03-31T19:42:01Z"
}
]
}Returns up to 500 leads attributed to a specific UTM value, ordered by earliest first touch (most recent first). Use this drilldown after listing campaigns from the aggregate endpoint to inspect contact-level detail, account linkage, and deal context.
| campaign_name required | string Exact UTM value to look up. URL-encode if it contains spaces or special characters. |
| group_by | string Default: "campaign" Enum: "campaign" "source" "medium" Which UTM key to match |
| start_date | string Lower bound for event |
| end_date | string Upper bound for event |
{- "campaign_name": "string",
- "leads": [
- {
- "lead_id": "9bddab70-98e6-43a9-8f32-c9788b9de0c0",
- "display_name": "string",
- "email": "string",
- "first_name": "string",
- "last_name": "string",
- "title": "string",
- "company": "string",
- "score": 0,
- "level": "string",
- "account_id": "449e7a5c-69d3-4b8a-aaaf-5c9b713ebc65",
- "account_name": "string",
- "deal_stage": "string",
- "deal_amount": 0,
- "first_touch_at": "2019-08-24T14:15:22Z",
- "last_touch_at": "2019-08-24T14:15:22Z"
}
]
}Returns scores for the provided lead_ids under the active OU. If ruleset is omitted or set to primary,
the primary engagement ruleset is used. If no primary exists, all scores are returned as 0.
| lead_ids required | Array of strings [ 1 .. 500 ] items |
| ruleset | string primary (default) or a specific ruleset_id/name |
{- "lead_ids": [
- "string"
], - "ruleset": "string"
}[- {
- "lead_id": "ld_001",
- "ruleset_id": "rs_abc",
- "ruleset_name": "Primary Engagement",
- "score": 32
}, - {
- "lead_id": "ld_002",
- "ruleset_id": "rs_abc",
- "ruleset_name": "Primary Engagement",
- "score": 18
}
]Reads a lead's score within the active OU. If ruleset_id is omitted, returns the highest score across all active rulesets for the active OU.
| leadID required | string |
{- "lead_id": "ld_123",
- "score": 87
}Returns lead scores for the active Organizational Unit (OU).
When page or limit is provided, the response includes pagination metadata (total, page, limit).
Notes:
| page | integer >= 1 Default: 1 |
| limit | integer [ 1 .. 500 ] Default: 50 |
| bot | string Enum: "hide" "only" Likely-bot view filter. |
| range | string Enum: "all" "today" "yesterday" "last_24h" "last_7d" "last_30d" "last_90d" Filter leads by created_at using a named range (e.g., last_7d). Also used for session counts. |
| min_score | integer Minimum total score. |
| max_score | integer Maximum total score. |
| event_type | string Filter by last event type per lead. |
| last_active | string Enum: "any" "7d" "30d" Filter by last activity recency (e.g., 7d or 30d). |
| include_disqualified | boolean Default: false When true, include disqualified leads. Defaults to false (qualified-only). |
| disposition | string Example: disposition=accepted_sql,nurture Filter by sales disposition. Comma-separated list of one or more of the six outcome values ( |
{- "leads": [
- {
- "lead_id": "ld_001",
- "score": 32
}, - {
- "lead_id": "ld_002",
- "score": 28
}
], - "total": 2,
- "page": 1,
- "limit": 25
}| id required | string |
object | |
| account_id | string or null |
{- "id": "string",
- "metadata": { },
- "account_id": "string"
}{- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "account_id": "string",
- "owner_id": "string",
- "owner_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "merged_into": "string",
- "metadata": { }
}Downloads all leads matching the provided filters as a CSV file. Respects the same filter parameters as GET /leads. The response is a CSV attachment with columns for lead ID, alias, engagement score, profile score, engagement level, profile level, account, owner, last activity date, and creation date.
| min_score | integer Minimum engagement score filter |
| max_score | integer Maximum engagement score filter |
| engagement_level | string Filter by engagement level name |
| profile_level | string Filter by profile level name |
| search | string Search by email, alias, or lead ID |
| last_active | string Filter by recency (e.g., 7d, 30d) |
| sort_by | string Field to sort by (e.g., score, created_at) |
| sort_order | string Enum: "asc" "desc" |
Returns up to 10 leads matching a search query. Matches by lead ID prefix or alias value (email, phone, etc.). Used for autocomplete in the UI.
| q required | string >= 2 characters Search query (minimum 2 characters) |
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "match_type": "id",
- "best_alias": "string",
- "engagement_level": "string",
- "profile_level": "string"
}
]| leadID required | string |
{- "lead": {
- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "account_id": "string",
- "owner_id": "string",
- "owner_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "merged_into": "string",
- "metadata": { }
}, - "account": { }
}| leadID required | string |
{- "lead_id": "string",
- "score": 0,
- "disqualified": true,
- "disqualified_by_rule_id": "string",
- "disqualified_at": "2019-08-24T14:15:22Z",
- "disqualified_reason": "string"
}Returns the ranked scoring factors (engagement, profile, account) for a single lead, along with a plain-language summary line suitable for display in the lead list.
When the lead is disqualified, the response short-circuits to the DQ phrase:
the factors array is empty and the disqualification block is populated.
This is the GDPR Art. 22 audit surface.
| leadID required | string |
{- "lead_id": "string",
- "summary": "string",
- "factors": [
- {
- "dimension": "engagement",
- "rule_name": "string",
- "contribution": 0,
- "matched_value": "string",
- "event_count": 0,
- "days_since_fresh": 0,
- "phrase": "string"
}
], - "disqualified": true,
- "disqualification": {
- "rule_name": "string",
- "reason": "string",
- "at": "2019-08-24T14:15:22Z"
}
}Returns aggregate sales-disposition feedback for the active Organizational Unit: total dispositions, distinct setters, the most active setter's share, per-outcome counts with average stamped score under the current scoring rules version, and up to 10 of the most recent rep notes.
Per-outcome average scores are restricted to dispositions stamped with the
OU's current primary engagement ruleset version (current_ruleset_version_id).
Merged-away leads and sample (demo) leads are excluded from every figure.
| since_days | integer [ 1 .. 365 ] Default: 90 Look-back window in days. Defaults to 90; clamped to 1–365. |
{- "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
}
]
}Compares the score kenbun gave each lead against the outcome sales
actually logged, so you can see where scoring agreed with sales and where
it did not. Powers the Score vs. Outcome view on the calibration
review page (Configure > Scoring).
The report has four parts:
Every figure uses the score stamped at the moment each outcome was logged. Merged-away leads and sample (demo) leads are excluded.
| since_days | integer [ 1 .. 365 ] Default: 90 Look-back window in days. Defaults to 90; clamped to 1–365. |
| high_score | integer >= 0 The score at or above which a rejected lead counts as a false positive. When omitted, kenbun derives it from your highest engagement level threshold, falling back to 75. |
| low_score | integer >= 0 Default: 40 The score below which a qualified or won lead counts as a false negative. Defaults to 40. |
| limit | integer [ 1 .. 100 ] Default: 25 Maximum leads returned in each of the false-positive and false-negative lists. Defaults to 25; clamped to 100. |
{- "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
}
]
}Streams the false-positive and false-negative leads from the disposition
quality report as a single CSV. A section column labels each row as
false_positive or false_negative. Accepts the same query parameters
as GET /leads/disposition-quality.
| since_days | integer [ 1 .. 365 ] Default: 90 |
| high_score | integer >= 0 |
| low_score | integer >= 0 Default: 40 |
| limit | integer [ 1 .. 100 ] Default: 25 |
Records a one-tap sales outcome on a lead. The lead's engagement score and scoring-rules version are stamped automatically at the moment the disposition is set, forming a labeled outcome record used by the calibration engine.
Setting a disposition never changes scoring weights — weight changes are a separate, human-approved action under Configure > Scoring.
Comment behavior: omit comment to preserve it on a same-outcome re-save (it
is cleared when the outcome changes); send an empty/whitespace string to clear
it; send text to set it (trimmed, max 2000 characters).
| leadID required | string <uuid> |
| disposition required | string Enum: "accepted_sql" "rejected_not_icp" "rejected_no_response" "nurture" "closed_won" "closed_lost" The sales outcome. |
| comment | string or null <= 2000 characters Optional rep note shown to marketing operations; never used in scoring math. |
{- "disposition": "accepted_sql",
- "comment": "Booked a demo"
}{- "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
}Removes the disposition from a lead, returning it to the "no disposition"
state. The response echoes the outcome that was cleared so callers can confirm
what was removed. Clearing a lead that had no disposition is a no-op success
with previous: null.
| leadID required | string <uuid> |
{- "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"
}
}Returns a plain-language "Why" summary string for each requested lead in one round-trip. Designed for the lead-list page where 50-200 rows each need a summary without issuing per-row API calls.
The endpoint issues at most 8 database queries regardless of N (the number of lead_ids), satisfying the O(1) query-count constraint for lead-list scale.
Disqualified leads short-circuit to their disqualification phrase only. This is the GDPR Art. 22 audit surface: a disqualified lead's "why" must always be the disqualification reason, never a stale engagement summary.
OU scoping: lead IDs that do not belong to the active OU appear in
missing[] without revealing whether the lead exists in another OU.
| lead_ids required | string Example: lead_ids=uuid-1,uuid-2,uuid-3 Comma-separated list of lead UUIDs. Maximum 200. Duplicates are silently de-duplicated. An empty value returns 200 with empty maps. |
{- "summaries": {
- "uuid-1": "Strong fit: VP at SaaS. Hot intent: pricing page 3x this week. Demo request 2 days ago.",
- "uuid-2": "Disqualified: no email AND no phone (today)."
}, - "missing": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}| leadID required | string |
{- "lead": {
- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "account_id": "string",
- "owner_id": "string",
- "owner_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "merged_into": "string",
- "metadata": { }
}, - "account": { }
}| leadID required | string |
object | |
| account_id | string or null |
{- "metadata": { },
- "account_id": "string"
}| id required | string |
object | |
| account_id | string or null |
{- "id": "string",
- "metadata": { },
- "account_id": "string"
}{- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "account_id": "string",
- "owner_id": "string",
- "owner_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "merged_into": "string",
- "metadata": { }
}| leadID required | string |
| property name* additional property | any |
{- "industry": "SaaS",
- "plan": "Pro"
}{- "id": "ld_123",
- "org_id": "org_1",
- "unit_id": "unit_default",
- "metadata": {
- "industry": "SaaS",
- "plan": "Pro"
}
}Resolves a lead from an alias such as an email address and returns the lead ID along with current metadata. Used by the Chrome extension Lead Profile tab to display the identified lead's profile attributes. OU-scoped -- only leads in the active Organizational Unit are searched.
| kind | string Default: "email" Alias type to search by. Defaults to "email". |
| value required | string The alias value to look up (e.g., the lead's email address). |
{- "lead_id": "ld_abc123",
- "email": "alex@example.com",
- "metadata": {
- "first_name": "Alex",
- "company": "Acme Corp",
- "quote_step": "Step 1"
}
}Resolves a lead from an alias (e.g., email) and merges the provided attributes into the lead's metadata. This is a silent update -- no event is created in the activity timeline. Used by the Chrome extension to annotate lead profiles with contextual information. OU-scoped -- the lead must exist in the active Organizational Unit.
| alias_kind | string Default: "email" Alias type. Defaults to "email". |
| alias_value required | string The alias value to resolve the lead (e.g., email address). |
object Key-value pairs to merge into the lead's metadata. Keys must be 1-64 characters and contain only letters, digits, underscores, dashes, dots, or spaces. Required if account_id is not provided. | |
| account_id | string Account ID to associate with this lead. Required if attributes is not provided. |
{- "alias_kind": "email",
- "alias_value": "alex@example.com",
- "attributes": {
- "quote_step": "Step 1",
- "interest_level": "high"
}
}{- "lead_id": "ld_abc123",
- "updated": 2,
- "attributes": {
- "quote_step": "Step 1",
- "interest_level": "high"
}, - "account_linked": false
}Consolidate multiple source leads into a target lead within the active OU. Moves events and aliases, marks sources as merged, and recomputes the target's lead scores. Cross-OU merges are not allowed. Default truncation policy is 'none'.
| targetLeadId required | string |
| sourceLeadIds required | Array of strings |
object |
{- "targetLeadId": "string",
- "sourceLeadIds": [
- "string"
], - "truncate": {
- "strategy": "none",
- "windowMinutes": 1,
- "keepTypes": [
- "string"
]
}
}{- "target": {
- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "account_id": "string",
- "owner_id": "string",
- "owner_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "merged_into": "string",
- "metadata": { }
}, - "mergedSources": [
- "string"
], - "movedEvents": 0,
- "truncatedEvents": 0,
- "rescore": {
- "mode": "inline",
- "taskId": "string"
}
}Returns top drivers, a rule weights snapshot, and surge boost contributions for the specified lead. OU scoped; time window defaults to last 24 hours.
| leadID required | string |
| hours | integer >= 1 Default: 24 |
{- "top_drivers": [
- {
- "rule_id": "string",
- "event_type": "string",
- "count": 0,
- "weight": 0,
- "total": 0,
- "filter_label": "string",
- "half_life_days": 0,
- "most_recent_at": "2019-08-24T14:15:22Z"
}
], - "snapshot": [
- {
- "event_type": "string",
- "weight": 0
}
], - "surge_boost": [
- {
- "base_event_type": "string",
- "total": 0,
- "events": 0,
- "avg_multiplier": 0
}
], - "surge_total": 0,
- "surge_event_count": 0,
- "decay_applied": 0,
- "inorganic_count": 0,
- "range_hours": 0,
- "score_reset_at": "2019-08-24T14:15:22Z",
- "reset_excluded_count": 0
}Zeros all engagement scores for the specified lead. Events and score history are preserved, but pre-reset event contributions are removed so backfills do not undo the reset. A score_reset event is logged in the timeline.
| leadID required | string |
{- "status": "ok",
- "lead_id": "string",
- "previous_scores": {
- "property1": 0,
- "property2": 0
}
}Assign a team member as the owner of a lead, or unassign the current owner by sending null values. The owner appears in lead list views and can be used for filtering and routing.
| leadID required | string <uuid> Lead identifier |
| owner_id | string or null User ID of the new owner. Send null to unassign. |
| owner_name | string or null Display name of the new owner |
{- "owner_id": "string",
- "owner_name": "string"
}{- "status": "ok",
- "message": "owner assigned"
}Assign an owner to multiple leads at once. Maximum 500 leads per request. Useful for routing batches of leads to a sales rep or reassigning leads in bulk.
| lead_ids required | Array of strings <= 500 items List of lead IDs to assign (max 500) |
| owner_id | string or null User ID of the owner. Send null to unassign. |
| owner_name | string or null Display name of the owner |
{- "lead_ids": [
- "string"
], - "owner_id": "string",
- "owner_name": "string"
}{- "status": "ok",
- "affected": 0
}| id required | string |
[- {
- "rule_id": "string",
- "ruleset_id": "string",
- "org_id": "string",
- "unit_id": "string",
- "event_type": "string",
- "weight": 0,
- "half_life_days": 1,
- "icon": "string",
- "active": true,
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}
]Atomically sets the priority order of all rules in a ruleset. The order of rule_ids in the request body becomes the new priority order (first element = highest priority). Returns 204 on success.
| id required | string Ruleset identifier |
| rule_ids required | Array of strings Rule IDs in the desired priority order (highest priority first) |
{- "rule_ids": [
- "rule-789",
- "rule-123",
- "rule-456"
]
}| rule_id | string |
| ruleset_id | string |
| org_id | string |
| unit_id | string |
| event_type | string |
| weight | integer |
| half_life_days | integer or null [ 1 .. 730 ] Score half-life in days. When set, an event scored N days ago contributes weight × 0.5^(N/half_life_days). Range [1, 730]. Omit or send null to disable decay for this rule. |
| icon | string |
| active | boolean |
| start_at | string <date-time> |
| end_at | string <date-time> |
| created_at | string <date-time> |
| updated_at | string <date-time> |
| created_by | string |
| updated_by | string |
{- "event_type": "Email Open",
- "weight": 5,
- "active": true
}{- "rule_id": "rl_2",
- "ruleset_id": "rs_default",
- "event_type": "Email Open",
- "weight": 5,
- "active": true
}| id required | string |
| rule_id | string |
| ruleset_id | string |
| org_id | string |
| unit_id | string |
| event_type | string |
| weight | integer |
| half_life_days | integer or null [ 1 .. 730 ] Score half-life in days. When set, an event scored N days ago contributes weight × 0.5^(N/half_life_days). Range [1, 730]. Omit or send null to disable decay for this rule. |
| icon | string |
| active | boolean |
| start_at | string <date-time> |
| end_at | string <date-time> |
| created_at | string <date-time> |
| updated_at | string <date-time> |
| created_by | string |
| updated_by | string |
{- "rule_id": "string",
- "ruleset_id": "string",
- "org_id": "string",
- "unit_id": "string",
- "event_type": "string",
- "weight": 0,
- "half_life_days": 1,
- "icon": "string",
- "active": true,
- "start_at": "2019-08-24T14:15:22Z",
- "end_at": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}Evaluates a scoring rule configuration against a specific lead without modifying production scores. Returns whether the rule would match and how many points it would award.
| lead_id required | string The lead to test the rule against |
required | object The rule configuration to evaluate |
{- "lead_id": "string",
- "rule": {
- "event_type": "string",
- "weight": 0,
- "attribute_filters": [
- {
- "field": "string",
- "operator": "equals",
- "value": "string"
}
]
}
}{- "matches": true,
- "reason": "string",
- "current_score": 0,
- "projected_score": 0,
- "matching_events": 0
}Immediately rescores all leads in the active OU against the specified ruleset. Works for any scoring dimension (engagement, profile, account, deal).
| dimension required | string Enum: "engagement" "profile" "account" "deal" The scoring dimension |
| id required | string The ruleset identifier |
Returns all surge triggers for the active organizational unit.
[- {
- "trigger_id": "string",
- "event_type": "string",
- "window_seconds": 1,
- "window_unit": "seconds",
- "threshold_count": 1,
- "notification_channel_id": "443c093d-58e9-4719-a2d4-12f031de4a66",
- "webhook_url": "string",
- "warnings": [
- "string"
], - "emails": [
- "user@example.com"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]Creates a new surge trigger for the active organizational unit.
| event_type | string Optional; leave empty to monitor all event types |
| window_seconds required | integer >= 1 |
| window_unit | string Default: "seconds" Enum: "seconds" "minutes" "hours" "days" |
| threshold_count required | integer >= 1 |
| active | boolean Default: true Whether the trigger is active. Defaults to true. Pass false to save a draft without a dispatch destination. |
| notification_channel_id | string or null <uuid> ID of a saved Notification Channel (scoped to the active OU). Must be a valid UUID that belongs to the active organizational unit. Preferred destination — supersedes webhook_url. |
| webhook_url | string or null Deprecated Deprecated. Legacy webhook destination. Use notification_channel_id instead. When set, the response includes a deprecation warning in the warnings field. |
| webhook_auth_header | string Optional authorization header for webhook requests |
| emails | Array of strings <email> [ items <email > ] |
| slack_target_kind | string Enum: "channel" "user" Slack destination type |
| slack_target_id | string Slack channel or user ID |
| teams_webhook_url | string Microsoft Teams incoming webhook URL |
{- "event_type": "string",
- "window_seconds": 1,
- "window_unit": "seconds",
- "threshold_count": 1,
- "active": true,
- "notification_channel_id": "443c093d-58e9-4719-a2d4-12f031de4a66",
- "webhook_url": "string",
- "webhook_auth_header": "string",
- "emails": [
- "user@example.com"
], - "slack_target_kind": "channel",
- "slack_target_id": "string",
- "teams_webhook_url": "string"
}{- "trigger_id": "string",
- "event_type": "string",
- "window_seconds": 1,
- "window_unit": "seconds",
- "threshold_count": 1,
- "notification_channel_id": "443c093d-58e9-4719-a2d4-12f031de4a66",
- "webhook_url": "string",
- "warnings": [
- "string"
], - "emails": [
- "user@example.com"
], - "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Updates an existing surge trigger by ID.
| id required | string |
| event_type | string Optional; leave empty to monitor all event types |
| window_seconds required | integer >= 1 |
| window_unit | string Default: "seconds" Enum: "seconds" "minutes" "hours" "days" |
| threshold_count required | integer >= 1 |
| active | boolean Default: true Whether the trigger is active. Defaults to true. Pass false to save a draft without a dispatch destination. |
| notification_channel_id | string or null <uuid> ID of a saved Notification Channel (scoped to the active OU). Must be a valid UUID that belongs to the active organizational unit. Preferred destination — supersedes webhook_url. |
| webhook_url | string or null Deprecated Deprecated. Legacy webhook destination. Use notification_channel_id instead. When set, the response includes a deprecation warning in the warnings field. |
| webhook_auth_header | string Optional authorization header for webhook requests |
| emails | Array of strings <email> [ items <email > ] |
| slack_target_kind | string Enum: "channel" "user" Slack destination type |
| slack_target_id | string Slack channel or user ID |
| teams_webhook_url | string Microsoft Teams incoming webhook URL |
{- "event_type": "string",
- "window_seconds": 1,
- "window_unit": "seconds",
- "threshold_count": 1,
- "active": true,
- "notification_channel_id": "443c093d-58e9-4719-a2d4-12f031de4a66",
- "webhook_url": "string",
- "webhook_auth_header": "string",
- "emails": [
- "user@example.com"
], - "slack_target_kind": "channel",
- "slack_target_id": "string",
- "teams_webhook_url": "string"
}Backtests a surge trigger against the last N days (up to 90) of event history for the active organizational unit. Returns the number of times the trigger would have fired and a sample list of matching lead IDs.
No notifications are sent and the trigger state is not modified.
| id required | string Surge trigger ID |
| days | integer [ 1 .. 90 ] Default: 30 Number of days of event history to scan (1–90, default 30) |
{- "days": 30
}{- "would_fire_count": 5,
- "sample_leads": [
- "ld_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- "ld_b2c3d4e5-f6a7-8901-bcde-f12345678901"
]
}Returns 24-hour delivery aggregate stats and the 10 most recent notification attempts for a surge trigger.
OU isolation: returns 404 when the trigger_id does not belong to the active OU, preventing cross-OU enumeration (ADR-0054).
| id required | string Surge trigger ID |
{- "delivered_24h": 0,
- "failed_24h": 3,
- "last_failure_at": "2026-05-12T01:12:13Z",
- "last_error": "connection timeout after 30s",
- "status": "red",
- "recent_attempts": [
- {
- "at": "2026-05-12T01:12:13Z",
- "outcome": "failed",
- "status": "retry_exhausted",
- "failure_stage": "dispatch",
- "error_excerpt": "connection timeout after 30s",
- "correlation_id": "corr-abc-123"
}
]
}Simulates a surge trigger against a single lead's event history and returns whether the trigger would have fired, how many times, and which specific time windows exceeded the threshold.
Unlike /preview (which scans all leads), this endpoint targets one lead and returns matched_windows for precise per-lead debugging and calibration.
No notifications are sent and the trigger state is not modified.
| id required | string Surge trigger ID |
| lead_id required | string The Lead ID to simulate against |
| days | integer [ 1 .. 90 ] Default: 30 Number of days of event history to scan (1–90, default 30) |
{- "lead_id": "ld_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- "days": 30
}{- "would_fire": true,
- "total_matches": 2,
- "message": "Trigger would have fired 2 time(s) for this lead in the last 30 days",
- "matched_windows": [
- {
- "window_start": "2025-01-10T14:00:00Z",
- "window_end": "2025-01-10T16:00:00Z",
- "event_count": 7
}, - {
- "window_start": "2025-01-15T09:00:00Z",
- "window_end": "2025-01-15T11:00:00Z",
- "event_count": 6
}
]
}Returns surge events for the active organizational unit. Surge events are recorded each time a surge trigger fires.
| page | integer >= 1 Default: 1 |
| limit | integer [ 1 .. 500 ] Default: 50 |
| start | string <date-time> Start timestamp (RFC3339). |
| end | string <date-time> End timestamp (RFC3339). |
| channel | string Filter by surge channel (snapshot channel/source). |
[ ]| name | string |
| score_column | string Default: "total_score" |
| is_primary | boolean Default: false |
{- "name": "Marketing",
- "score_column": "total_score"
}{- "ruleset_id": "rs_marketing",
- "name": "Marketing",
- "score_column": "total_score"
}Evaluates a profile scoring rule against a specific lead without modifying production scores.
| lead_id required | string The lead to test the rule against |
required | object The profile rule configuration to evaluate |
{- "lead_id": "string",
- "rule": {
- "profile_property": "string",
- "condition": "equals",
- "comparison_value": "string",
- "weight": 0
}
}{- "matches": true,
- "reason": "string",
- "current_score": 0,
- "projected_score": 0,
- "matching_value": "string"
}| source_field required | string |
| profile_property required | string |
{- "source_field": "utm_source",
- "profile_property": "acquisition_source"
}{- "id": "pm_2",
- "source_field": "utm_source",
- "profile_property": "acquisition_source"
}| id required | string |
| source_field required | string |
| profile_property required | string |
{- "source_field": "string",
- "profile_property": "string"
}[- {
- "id": "string",
- "org_id": "string",
- "name": "string",
- "enabled": true,
- "is_primary": true,
- "score_floor_pct": 0.5,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| name required | string |
| enabled | boolean |
| is_primary | boolean Default: false |
{- "name": "string",
- "enabled": true,
- "is_primary": false
}{- "id": "string",
- "org_id": "string",
- "name": "string",
- "enabled": true,
- "is_primary": true,
- "score_floor_pct": 0.5,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string |
| name required | string |
| enabled | boolean |
| is_primary | boolean Default: false |
{- "name": "string",
- "enabled": true,
- "is_primary": false
}| id required | string |
[- {
- "id": "string",
- "ruleset_id": "string",
- "profile_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "half_life_days": 1,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}, - "ui_order": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| id required | string |
| profile_property required | string |
| condition required | string |
| comparison_value required | string |
| weight required | integer |
| ui_order | integer |
| rule_type | string Enum: "add_points" "hard_disqualify" Defaults to add_points when omitted. |
object or null Compound AND/OR condition group. Only valid when rule_type is hard_disqualify. |
{- "profile_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "ui_order": 0,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}
}{- "id": "string",
- "ruleset_id": "string",
- "profile_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "half_life_days": 1,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}, - "ui_order": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string |
| ruleId required | string |
| profile_property | string |
| condition | string |
| comparison_value | string |
| weight | integer |
| ui_order | integer |
| rule_type | string Enum: "add_points" "hard_disqualify" |
object or null Compound AND/OR condition group. Only valid when rule_type is hard_disqualify. |
{- "profile_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "ui_order": 0,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}
}Returns the audit log of all profile mapping changes — creates, updates, and deletes — for the active OU.
[- {
- "id": "string",
- "mapping_type": "string",
- "action": "created",
- "actor": "string",
- "old_value": { },
- "new_value": { },
- "created_at": "2019-08-24T14:15:22Z"
}
]Returns the number of leads that appear in both segments, plus counts for each segment individually.
| a required | string ID of the first segment |
| b required | string ID of the second segment |
{- "a_count": 0,
- "b_count": 0,
- "overlap_count": 0,
- "overlap_pct_a": 0,
- "overlap_pct_b": 0
}| name required | string |
| description | string |
| active | boolean |
{- "name": "string",
- "description": "string",
- "active": true
}{- "segment_id": "sg_marketing",
- "name": "Marketing Leads",
- "description": "Leads from marketing sources",
- "active": true
}| id required | string |
| name required | string |
| description | string |
| active | boolean |
{- "name": "string",
- "description": "string",
- "active": true
}| id required | string |
| include_disqualified | boolean Default: false When true, include disqualified leads in the count. Defaults to false (qualified-only). |
{- "count": 0
}| id required | string |
| page | integer >= 1 Default: 1 |
| limit | integer [ 1 .. 500 ] Default: 50 |
| include_disqualified | boolean Default: false When true, include disqualified leads. Defaults to false (qualified-only). |
[- {
- "org_id": "string",
- "lead_id": "string",
- "ruleset_id": "string",
- "score": 0,
- "last_event_at": "2019-08-24T14:15:22Z",
- "profile_scores": {
- "property1": 0,
- "property2": 0
}, - "ruleset_names": {
- "property1": "string",
- "property2": "string"
}, - "profile_primary_ruleset_name": "string",
- "engagement_scores": {
- "property1": 0,
- "property2": 0
}, - "engagement_primary_ruleset_name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "owner_name": "string",
- "account_id": "string",
- "engagement_level": "string",
- "profile_level": "string",
- "account_level": "string",
- "best_alias": "string"
}
]| id required | string |
[- {
- "rule_id": "string",
- "segment_id": "string",
- "org_id": "string",
- "rule_name": "string",
- "rule_type": "lead_metadata",
- "field_name": "string",
- "operator": "string",
- "value": "string",
- "event_type": "string",
- "logic_group": 0,
- "logic_op": "AND",
- "order_index": 0,
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}
]| id required | string |
| rule_name | string |
| rule_type required | string Enum: "lead_metadata" "event_metadata" |
| field_name required | string |
| operator required | string |
| value | string |
| event_type | string |
| logic_group | integer |
| logic_op | string Enum: "AND" "OR" |
| order_index | integer |
| active | boolean |
{- "rule_name": "string",
- "rule_type": "lead_metadata",
- "field_name": "string",
- "operator": "string",
- "value": "string",
- "event_type": "string",
- "logic_group": 0,
- "logic_op": "AND",
- "order_index": 0,
- "active": true
}{- "rule_id": "string",
- "segment_id": "string",
- "org_id": "string",
- "rule_name": "string",
- "rule_type": "lead_metadata",
- "field_name": "string",
- "operator": "string",
- "value": "string",
- "event_type": "string",
- "logic_group": 0,
- "logic_op": "AND",
- "order_index": 0,
- "active": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| ruleId required | string |
| rule_name | string |
| rule_type | string Enum: "lead_metadata" "event_metadata" |
| field_name | string |
| operator | string |
| value | string |
| event_type | string |
| logic_group | integer |
| logic_op | string Enum: "AND" "OR" |
| order_index | integer |
| active | boolean |
{- "rule_name": "string",
- "rule_type": "lead_metadata",
- "field_name": "string",
- "operator": "string",
- "value": "string",
- "event_type": "string",
- "logic_group": 0,
- "logic_op": "AND",
- "order_index": 0,
- "active": true
}| inactive_for required | integer Duration amount |
| unit required | string Enum: "days" "hours" "weeks" |
| decrease required | integer Score decrease amount |
{- "inactive_for": 0,
- "unit": "days",
- "decrease": 0
}{- "decay_id": "dc_inactive30d",
- "inactive_for": 30,
- "unit": "days",
- "decrease": 25
}| id required | string |
| inactive_for | integer |
| unit | string Enum: "days" "hours" "weeks" |
| decrease | integer |
{- "inactive_for": 0,
- "unit": "days",
- "decrease": 0
}Lists configured inorganic activity filters used to flag bot-like activity in the active OU.
[- {
- "filter_id": "string",
- "event_type": "string",
- "count_threshold": 0,
- "time_window_ms": 0,
- "group_events": true,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}
]| event_type required | string |
| count_threshold required | integer |
| time_window_ms required | integer |
| group_events | boolean |
{- "event_type": "string",
- "count_threshold": 0,
- "time_window_ms": 0,
- "group_events": true
}{- "filter_id": "string",
- "event_type": "string",
- "count_threshold": 0,
- "time_window_ms": 0,
- "group_events": true,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| event_type | string |
| count_threshold | integer |
| time_window_ms | integer |
| group_events | boolean |
{- "event_type": "string",
- "count_threshold": 0,
- "time_window_ms": 0,
- "group_events": true
}Attribute based filters suppress engagement events whose metadata matches key/value conditions. Evaluated before velocity based filters.
Returns all metadata attribute filters for the active OU, sorted enabled-first then newest-first.
[- {
- "id": "string",
- "unit_id": "string",
- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true,
- "suppressed_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}
]| name required | string Human-readable label for the filter |
| event_type | string Scope to a specific event type. Empty string matches all types. |
Array of objects (MetadataAttributeFilterCondition) | |
| enabled | boolean Default: true |
{- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true
}{- "id": "string",
- "unit_id": "string",
- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true,
- "suppressed_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}Runs the provided conditions against existing events and returns how many would be suppressed. Does not persist any changes.
| event_type | string Scope preview to a specific event type. Omit for all types. |
required | Array of objects (MetadataAttributeFilterCondition) |
{- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
]
}{- "count": 0
}| id required | string |
{- "id": "string",
- "unit_id": "string",
- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true,
- "suppressed_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| name required | string |
| event_type | string |
Array of objects (MetadataAttributeFilterCondition) | |
| enabled | boolean |
{- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true
}{- "id": "string",
- "unit_id": "string",
- "name": "string",
- "event_type": "string",
- "conditions": [
- {
- "key": "string",
- "operator": "equals",
- "value": "string"
}
], - "enabled": true,
- "suppressed_count": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}Scoring endpoints compute and explain lead, profile, and account scores. POST /scoring/score is a synchronous, multi-dimension scorer that returns the full result (score, level, drivers, disqualification) in the response body. All endpoints are organization- and OU-scoped; org and OU are derived from the authenticated context.
Synchronous, in-process multi-dimension scorer. Unlike POST /ingest (which returns 202 and scores asynchronously), this endpoint computes the score in the request path and returns the full result — score, level, drivers, and disqualification status — in a 200 body.
The lead is resolved-or-created by external_id (alias kind
external_id). At least one scorable dimension is required: event
(engagement), profile (firmographic fit), and/or account (company
fit). Only the dimensions you POST are scored and returned. Deal scoring
is out of scope.
Each dimension's score is the deterministic score of the POSTed object under its ruleset — NOT the lead's cumulative decaying lifetime total. Identical input yields identical output.
Enrichment (persistence). Beyond scoring, this endpoint can also write the data you POST onto the lead and its company:
profile is supplied, its firmographic attributes are merged
onto the lead's stored profile/metadata. This requires the token to
also hold the write:leads scope; otherwise the request fails with
403 insufficient_scope.account is supplied, a company (account) is resolved-or-created
and the lead is linked to it. Resolution order is: the account's
external_id (CRM company id) if provided, else its domain, else
its name. The POSTed firmographics are merged onto the account, and
the linked account id is returned as account_id. This requires the
token to also hold the write:accounts scope; otherwise the request
fails with 403 insufficient_scope.The response's enriched object reports which records were persisted
(for example, {"lead": true, "account": true}). An event-only call
scores without persisting profile or account data and needs only
write:scoring; a full create-score-enrich call needs
write:scoring, write:leads, and write:accounts.
Honest results: a genuine score of 0 returns 200; a disqualified lead
returns 200 with disqualified: true; failures return 4xx/5xx so callers
can always distinguish "scored zero" from "could not score".
Idempotent: pass idempotency_key (for example, an n8n execution id) to
make retries safe. A replay returns the stored result with
cached: true. If omitted, a key is derived from the request contents.
Authentication: a Personal Access Token (PAT) with the write:scoring
scope (plus write:leads and/or write:accounts when posting profile
or account, see above). The server derives org_id and unit_id from the
token; clients must not send unit_id.
| external_id required | string External identifier used to resolve or create the lead (alias kind external_id). |
| idempotency_key | string Optional. Make retries safe; a replay returns the stored result with cached:true. Derived from request contents if omitted. |
| triggers_enabled | boolean Default: false When true, downstream notifications/automations may fire for the engagement signal. Defaults to false. |
| source | string Default: "score" Optional traffic source label. Defaults to 'score'. The reserved values 'test' and 'sample_data' are rejected. |
object (ScoreSyncRulesets) Optional per-dimension ruleset names. Omit a dimension to use its primary ruleset. Never more than one ruleset per dimension. | |
object (ScoreSyncEvent) An engagement signal to score against the engagement ruleset. kenbun
events are channel-agnostic: the event itself carries only what kind of
thing happened ( | |
object Firmographic profile attributes (role, company_size, industry,
icp_tier, ...). Scored against the profile ruleset. When supplied,
these attributes are also merged onto the lead's stored profile,
which requires the | |
object Company attributes. Scored against the account ruleset. When
supplied, a company (account) is resolved-or-created and the lead is
linked to it, which requires the |
{- "external_id": "linkedin:12345",
- "idempotency_key": "n8n-exec-abc",
- "source": "n8n",
- "event": {
- "event_type": "hiring_post",
- "occurred_at": "2026-06-06T12:00:00Z",
- "metadata": {
- "channel": "linkedin",
- "text": "We are hiring 5 SDRs",
- "seniority": "vp"
}
}
}{- "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": "role",
- "condition": "equals",
- "value": "VP Sales",
- "weight": 40,
- "matched": true
}
]
}
}Returns every active hard-disqualification rule in the active OU across
both profile and account dimensions, together with the current count of
leads flagged by each rule (lead_scores.disqualified = TRUE).
Rules with zero matches are included so the UI can display all configured rules regardless of current activity.
Results are ordered by match_count DESC, rule_name ASC.
[- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "dimension": "profile",
- "rule_name": "string",
- "match_count": 42
}
]Returns paper §3 (B2B SaaS practitioner table) half-life defaults for
rules in the active OU that currently have half_life_days IS NULL.
Only engagement rules are eligible for suggestions; profile and account rules are firmographic-only and excluded per ADR-0048. Deal rules are fetched but always return ineligible (no event-type decay table applies).
The eligible_count field is the count to display on the "Apply research
defaults" button label in the UI.
| dimension | string Default: "all" Enum: "engagement" "all" Limit results to the engagement dimension. |
{- "eligible_count": 0,
- "ineligible_count": 0,
- "suggestions": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "event_type": "page_view",
- "current_half_life_days": 0,
- "suggested_half_life_days": 14,
- "source": "paper §3 (B2B SaaS practitioner table)"
}
]
}Applies paper §3 half-life values to each rule in rule_ids. Each
rule's outcome is independent — a per-rule failure does not roll back
other rules that have already been updated.
Skip reasons:
already_configured — rule already has a non-null half_life_days.no_research_default — event type not in the practitioner table, or
rule has no event_type (session-based).dimension_not_event_sourced — rule belongs to a profile, account,
or deal ruleset. These dimensions are current-state evaluation and do
not support half-life decay. Per ADR-0048 scoping correction.not_found — rule does not exist or belongs to a different OU.lookup_error — transient DB error during lookup.update_error — transient DB error during write.Audit trail: every applied rule writes a granular audit log entry
with actor = authenticated_user per ADR-0047 (GDPR Art. 22).
The request itself also generates a summary entry via the audit middleware.
| rule_ids required | Array of strings <uuid> non-empty [ items <uuid > ] Rule IDs to apply research defaults to. Must be non-empty. |
{- "rule_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}{- "applied_count": 0,
- "skipped_count": 0,
- "results": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "dimension": "engagement",
- "applied_half_life_days": 0,
- "status": "applied",
- "reason": "already_configured"
}
]
}Reports how close the active Organizational Unit is to producing scoring
weight suggestions from sales outcomes. Powers the readiness strip on the
calibration review card (Configure > Scoring).
It returns the number of qualifying logged outcomes in the look-back window, the 30-outcome floor that unlocks suggestions, the scored-lead cohort and its 50-lead floor, how many new outcomes have been logged since calibration last ran, and when it last ran.
When the OU has no conversion definition configured, a zero-state response is returned with the standard floors.
{- "conversions_in_window": 22,
- "floor": 30,
- "cohort_size": 140,
- "cohort_floor": 50,
- "new_dispositions_since_last_run": 6,
- "last_run_at": "2026-06-09T07:00:00Z",
- "floor_crossed_notified_at": null,
- "progress_nudged_at": "2026-06-02T07:00:00Z"
}Requests an immediate recalibration for the active Organizational Unit instead of waiting for the next weekly run. Powers the Recalibrate Now button on the calibration review card.
The request returns immediately; recalibration runs automatically in the background and updated suggestions appear on the calibration review page when it finishes. The OU must have a conversion definition configured.
Returns the cached or freshly-computed calibration lift estimate for a single engagement scoring rule.
The ?force=1 query parameter forces a full recompute via ComputeAll
and persists the result before returning.
State variants:
no_conversion_definition — OU has no conversion definition configured.insufficient_cohort — fewer than 50 leads scored in the lookback window.insufficient_conversions — the conversion definition is disposition-fed
(sales_disposition or manual_label) and fewer than 30 qualifying
conversions exist. Includes conversions_observed, conversions_required
(30), and conversion_type.never_computed — no cached result and force=1 was not passed, or the
rule had no activity in the lookback window.ready — calibration result present (possibly stale).| rulesetId required | string <uuid> |
| ruleId required | string <uuid> |
| force | string Value: "1" Pass |
{- "state": "no_conversion_definition",
- "required_action": "set_definition_in_ou_settings",
- "leads_scored": 0,
- "leads_required": 0,
- "conversions_observed": 0,
- "conversions_required": 30,
- "conversion_type": "sales_disposition",
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "current_weight": 0,
- "suggested_weight": 0,
- "sample_size": 0,
- "conversions_count": 0,
- "baseline_cr": 0.1,
- "rule_cr": 0.1,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
| ruleId required | string <uuid> |
| force | string Value: "1" |
{- "state": "no_conversion_definition",
- "required_action": "set_definition_in_ou_settings",
- "leads_scored": 0,
- "leads_required": 0,
- "conversions_observed": 0,
- "conversions_required": 30,
- "conversion_type": "sales_disposition",
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "current_weight": 0,
- "suggested_weight": 0,
- "sample_size": 0,
- "conversions_count": 0,
- "baseline_cr": 0.1,
- "rule_cr": 0.1,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
| ruleId required | string <uuid> |
| force | string Value: "1" |
{- "state": "no_conversion_definition",
- "required_action": "set_definition_in_ou_settings",
- "leads_scored": 0,
- "leads_required": 0,
- "conversions_observed": 0,
- "conversions_required": 30,
- "conversion_type": "sales_disposition",
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "current_weight": 0,
- "suggested_weight": 0,
- "sample_size": 0,
- "conversions_count": 0,
- "baseline_cr": 0.1,
- "rule_cr": 0.1,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
| ruleId required | string <uuid> |
| force | string Value: "1" |
{- "state": "no_conversion_definition",
- "required_action": "set_definition_in_ou_settings",
- "leads_scored": 0,
- "leads_required": 0,
- "conversions_observed": 0,
- "conversions_required": 30,
- "conversion_type": "sales_disposition",
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "current_weight": 0,
- "suggested_weight": 0,
- "sample_size": 0,
- "conversions_count": 0,
- "baseline_cr": 0.1,
- "rule_cr": 0.1,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}Returns all cached calibration runs for the given engagement scoring ruleset, joined with current rule metadata (name, weight).
The response includes a conversion_definition_summary block when the
OU has a conversion definition configured, and an empty runs array
when no calibration has been computed yet.
| rulesetId required | string <uuid> |
{- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "conversion_definition_summary": {
- "conversion_type": "closed_won",
- "lookback_days": 0,
- "deal_stages": [
- "string"
]
}, - "runs": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "rule_name": "string",
- "current_weight": 0,
- "suggested_weight": 0,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "sample_size": 0,
- "conversions_count": 0,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}
]
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
{- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "conversion_definition_summary": {
- "conversion_type": "closed_won",
- "lookback_days": 0,
- "deal_stages": [
- "string"
]
}, - "runs": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "rule_name": "string",
- "current_weight": 0,
- "suggested_weight": 0,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "sample_size": 0,
- "conversions_count": 0,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}
]
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
{- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "conversion_definition_summary": {
- "conversion_type": "closed_won",
- "lookback_days": 0,
- "deal_stages": [
- "string"
]
}, - "runs": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "rule_name": "string",
- "current_weight": 0,
- "suggested_weight": 0,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "sample_size": 0,
- "conversions_count": 0,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}
]
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| rulesetId required | string <uuid> |
{- "ruleset_id": "ca688a19-2766-4b2c-aefa-28d16a464773",
- "dimension": "engagement",
- "conversion_definition_summary": {
- "conversion_type": "closed_won",
- "lookback_days": 0,
- "deal_stages": [
- "string"
]
}, - "runs": [
- {
- "rule_id": "728c1541-d6d1-4290-9a53-cdf01dd32d60",
- "rule_name": "string",
- "current_weight": 0,
- "suggested_weight": 0,
- "raw_lift": 0.1,
- "shrunk_lift": 0.1,
- "sample_size": 0,
- "conversions_count": 0,
- "confidence": "insufficient",
- "co_occurrence_warnings": [
- {
- "other_rule_id": "0f18107a-e362-4e15-b00b-6c47ab5bc649",
- "overlap_pct": 0.1
}
], - "computed_at": "2019-08-24T14:15:22Z",
- "stale": true
}
]
}Applies a cached calibration run's suggested weight to the engagement scoring rule, writing a fully-attributed audit entry in the same transaction.
Rejection paths:
404 rule_not_found — rule does not exist or belongs to a different OU.400 no_conversion_definition — OU has no conversion definition.400 never_computed — no calibration run found; call GET ?force=1 first.409 stale_calibration — computed_at is older than 14 days.400 insufficient_sample — suggested_weight is nil (low confidence).409 insufficient_conversions — disposition-fed definition
(sales_disposition or manual_label) re-checked at apply time and fewer
than 30 qualifying conversions exist. This apply-time floor is the governance
backstop: a cached suggestion computed before disposition volume dropped can
render but can never become a weight. No weight is written.| ruleId required | string <uuid> |
{- "prior_weight": 0,
- "new_weight": 0,
- "audit_id": "78c04fa6-cfb4-46a0-9aa5-3681ba4f3897"
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| ruleId required | string <uuid> |
{- "prior_weight": 0,
- "new_weight": 0,
- "audit_id": "78c04fa6-cfb4-46a0-9aa5-3681ba4f3897"
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| ruleId required | string <uuid> |
{- "prior_weight": 0,
- "new_weight": 0,
- "audit_id": "78c04fa6-cfb4-46a0-9aa5-3681ba4f3897"
}Identical semantics to the engagement variant. See that endpoint for full documentation.
| ruleId required | string <uuid> |
{- "prior_weight": 0,
- "new_weight": 0,
- "audit_id": "78c04fa6-cfb4-46a0-9aa5-3681ba4f3897"
}Lists pattern triggers (including frequency counts) for the active OU.
[- {
- "trigger_id": "string",
- "name": "string",
- "description": "string",
- "active": true,
- "window_seconds": 0,
- "conditions": [
- {
- "type": "count",
- "event_type": "string",
- "count": 0,
- "operator": "gte"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| trigger_id | string |
| name | string |
| description | string |
| active | boolean |
| window_seconds | integer |
Array of objects | |
| webhook_url | string |
| emails | Array of strings <email> [ items <email > ] |
| created_at | string <date-time> |
| updated_at | string <date-time> |
{- "trigger_id": "string",
- "name": "string",
- "description": "string",
- "active": true,
- "window_seconds": 0,
- "conditions": [
- {
- "type": "count",
- "event_type": "string",
- "count": 0,
- "operator": "gte"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}{- "trigger_id": "string",
- "name": "string",
- "description": "string",
- "active": true,
- "window_seconds": 0,
- "conditions": [
- {
- "type": "count",
- "event_type": "string",
- "count": 0,
- "operator": "gte"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string |
| trigger_id | string |
| name | string |
| description | string |
| active | boolean |
| window_seconds | integer |
Array of objects | |
| webhook_url | string |
| emails | Array of strings <email> [ items <email > ] |
| created_at | string <date-time> |
| updated_at | string <date-time> |
{- "trigger_id": "string",
- "name": "string",
- "description": "string",
- "active": true,
- "window_seconds": 0,
- "conditions": [
- {
- "type": "count",
- "event_type": "string",
- "count": 0,
- "operator": "gte"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns a paginated list of notification failure records for the active Organizational Unit. Results are ordered by last_attempt_at DESC, id DESC. Use next_cursor to fetch subsequent pages. All filters are optional; omitting them returns the last 7 days of failures.
| channel_id | string <uuid> Filter to failures for a specific notification channel. |
| trigger_id | string <uuid> Filter to failures associated with a specific trigger. |
| trigger_type | string Enum: "milestone" "sequence" "surge" Filter to failures for a specific trigger type. |
| status | string Comma-separated list of statuses to include. Valid values: enqueue_failed, retry_exhausted, terminal_4xx, delivered. |
| failure_stage | string Enum: "enqueue" "dispatch" "surge_webhook" "probe" Filter to failures at a specific pipeline stage. |
| since | string <date-time> RFC3339 timestamp. Only return failures where last_attempt_at >= since. Defaults to 7 days ago when omitted. |
| limit | integer [ 1 .. 200 ] Default: 50 Page size. Maximum 200. Returns 400 if exceeded. |
| cursor | string Opaque pagination cursor returned as next_cursor from a previous response. |
{- "items": [
- {
- "id": 0,
- "channel": "string",
- "trigger_type": "milestone",
- "trigger_id": "5727dbbb-3b26-4abe-aec6-181eabbdb21c",
- "status": "enqueue_failed",
- "failure_stage": "enqueue",
- "last_error": "string",
- "attempts": 0,
- "correlation_id": "string",
- "received_at": "2019-08-24T14:15:22Z",
- "last_attempt_at": "2019-08-24T14:15:22Z"
}
], - "next_cursor": "string",
- "total_count": 0
}Returns all saved notification channels for the active Organizational Unit. Channels can be referenced when creating Milestone or Sequence Triggers.
[- {
- "id": "string",
- "unit_id": "string",
- "name": "string",
- "type": "slack",
- "config": {
- "property1": "string",
- "property2": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| name required | string Display name for the channel |
| type required | string Enum: "slack" "teams" "webhook" "email" |
required | object Type-specific configuration fields |
{- "name": "string",
- "type": "slack",
- "config": {
- "property1": "string",
- "property2": "string"
}
}| id required | string |
| name | string |
| type | string Enum: "slack" "teams" "webhook" "email" |
object |
{- "name": "string",
- "type": "slack",
- "config": {
- "property1": "string",
- "property2": "string"
}
}Returns a health summary for a single notification channel covering both a 24-hour window (delivered_24h, failed_24h, status) and a 7-day window (failure_count_7d, last_failure_at, last_error, is_healthy, permanent_failure). The channel must belong to the caller's active Organizational Unit; a missing or cross-OU channel returns 404 to avoid existence leaking. delivered_24h limitation: currently always 0. The successful-delivery counter is a follow-up; until it ships, statuses green and amber (which require a non-zero delivered count) are unreachable. The meaningful states today are gray (no failures recorded) and red (at least one failure recorded with no tracked deliveries). The 7-day fields are the primary signal for the UI badge — they capture the "Bot not in channel — Fix" red state (permanent_failure) and the "had a transient blip" amber state (failure_count_7d > 0, permanent_failure=false).
| id required | string <uuid> Notification channel UUID |
{- "channel_id": "bbe8aa76-a4bb-46f6-a785-df8e831cc459",
- "delivered_24h": 0,
- "failed_24h": 0,
- "failure_count_7d": 0,
- "last_failure_at": "2019-08-24T14:15:22Z",
- "last_error": "string",
- "is_healthy": true,
- "permanent_failure": true,
- "status": "green"
}Sends a one-off "test" message through the configured channel so an administrator can confirm end-to-end delivery without firing a real trigger. Slack channels post a real chat.postMessage, webhook channels receive a signed JSON ping, and Teams returns 501 (test sends for Teams are not implemented yet — fire a real trigger or contact support). Every attempt — success or failure — writes an audit_logs row with extra.action="notification_channel_tested" so admins can see who pinged what when. Admin-only at the route layer; non-admins receive 403.
| id required | string <uuid> Notification channel UUID |
No request body. The endpoint takes the channel UUID from the path and the actor from the session.
{ }{- "ok": true,
- "message_ts": "string",
- "channel_id": "string",
- "channel_type": "slack",
- "error": "string",
- "message": "string"
}Lists milestone triggers configured for the active OU.
[- {
- "trigger_id": "string",
- "org_id": "string",
- "event_type": "string",
- "score_threshold": 0,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}
]| event_type required | string |
| score_threshold required | integer |
| webhook_url | string |
| emails | Array of strings <email> [ items <email > ] |
| email_subject | string |
| email_preheader | string |
| email_message | string |
{- "event_type": "string",
- "score_threshold": 0,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string"
}{- "trigger_id": "string",
- "org_id": "string",
- "event_type": "string",
- "score_threshold": 0,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string",
- "name": "string",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| event_type | string |
| score_threshold | integer |
| webhook_url | string |
| emails | Array of strings <email> [ items <email > ] |
| email_subject | string |
| email_preheader | string |
| email_message | string |
{- "event_type": "string",
- "score_threshold": 0,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string"
}Returns 24-hour delivery aggregate stats and the 10 most recent notification attempts for a milestone trigger.
OU isolation: returns 404 when the trigger_id does not belong to the active OU, preventing cross-OU enumeration (ADR-0054).
| id required | string Milestone trigger ID |
{- "delivered_24h": 0,
- "failed_24h": 3,
- "last_failure_at": "2026-05-12T01:12:13Z",
- "last_error": "connection timeout after 30s",
- "status": "red",
- "recent_attempts": [
- {
- "at": "2026-05-12T01:12:13Z",
- "outcome": "failed",
- "status": "retry_exhausted",
- "failure_stage": "dispatch",
- "error_excerpt": "connection timeout after 30s",
- "correlation_id": "corr-abc-123"
}
]
}Test a milestone trigger against a specific lead or scan historical data. Returns whether the trigger would fire and which leads match.
| id required | string |
| lead_id | string Specific lead to test (optional) |
| days | integer Default: 30 Days to look back |
{- "lead_id": "string",
- "days": 30
}{- "would_fire": true,
- "matched_leads": [
- "string"
], - "total_matches": 0,
- "message": "string"
}Lists time- or order-based sequence triggers for the active OU.
[- {
- "trigger_id": "string",
- "org_id": "string",
- "unit_id": "string",
- "name": "string",
- "event_steps": [
- {
- "event_type": "string",
- "attribute_filters": [
- {
- "field": "string",
- "operator": "equals",
- "value": "string"
}
]
}
], - "window_seconds": 0,
- "window_unit": "seconds",
- "ordered": true,
- "fire_mode": "once",
- "profile_filters": [
- {
- "field": "engagement_level",
- "operator": "eq",
- "value": "string"
}
], - "lead_attribute_filters": [
- {
- "field": "string",
- "operator": "equals",
- "value": "string"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string",
- "slack_target_kind": "channel",
- "slack_target_id": "string",
- "teams_target_kind": "channel",
- "teams_target_id": "string",
- "hubspot_sync": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}
]| event_types required | Array of strings |
| window_seconds required | integer |
| window_unit required | string Enum: "seconds" "minutes" "hours" |
| ordered | boolean |
| webhook_url required | string |
| emails | Array of strings <email> [ items <email > ] |
| email_subject | string |
| email_preheader | string |
| email_message | string |
{- "event_types": [
- "string"
], - "window_seconds": 0,
- "window_unit": "seconds",
- "ordered": true,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string"
}{- "trigger_id": "string",
- "org_id": "string",
- "unit_id": "string",
- "name": "string",
- "event_steps": [
- {
- "event_type": "string",
- "attribute_filters": [
- {
- "field": "string",
- "operator": "equals",
- "value": "string"
}
]
}
], - "window_seconds": 0,
- "window_unit": "seconds",
- "ordered": true,
- "fire_mode": "once",
- "profile_filters": [
- {
- "field": "engagement_level",
- "operator": "eq",
- "value": "string"
}
], - "lead_attribute_filters": [
- {
- "field": "string",
- "operator": "equals",
- "value": "string"
}
], - "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string",
- "slack_target_kind": "channel",
- "slack_target_id": "string",
- "teams_target_kind": "channel",
- "teams_target_id": "string",
- "hubspot_sync": true,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| event_types | Array of strings |
| window_seconds | integer |
| window_unit | string Enum: "seconds" "minutes" "hours" |
| ordered | boolean |
| webhook_url | string |
| emails | Array of strings <email> [ items <email > ] |
| email_subject | string |
| email_preheader | string |
| email_message | string |
{- "event_types": [
- "string"
], - "window_seconds": 0,
- "window_unit": "seconds",
- "ordered": true,
- "webhook_url": "string",
- "emails": [
- "user@example.com"
], - "email_subject": "string",
- "email_preheader": "string",
- "email_message": "string"
}Returns 24-hour delivery aggregate stats and the 10 most recent notification attempts for a sequence trigger.
OU isolation: returns 404 when the trigger_id does not belong to the active OU, preventing cross-OU enumeration (ADR-0054).
| id required | string Sequence trigger ID |
{- "delivered_24h": 0,
- "failed_24h": 3,
- "last_failure_at": "2026-05-12T01:12:13Z",
- "last_error": "connection timeout after 30s",
- "status": "red",
- "recent_attempts": [
- {
- "at": "2026-05-12T01:12:13Z",
- "outcome": "failed",
- "status": "retry_exhausted",
- "failure_stage": "dispatch",
- "error_excerpt": "connection timeout after 30s",
- "correlation_id": "corr-abc-123"
}
]
}Test a sequence trigger against a specific lead or scan historical data. Returns whether the trigger would fire and which leads match.
| id required | string |
| lead_id | string Specific lead to test (optional) |
| days | integer Default: 30 Days to look back |
{- "lead_id": "string",
- "days": 30
}{- "would_fire": true,
- "matched_leads": [
- "string"
], - "total_matches": 0,
- "message": "string"
}| id required | string |
| name required | string |
{- "name": "string"
}{- "unit_id": "string",
- "name": "string",
- "slug": "string"
}Validates membership and sets the activeOu cookie. Returns OU headers for convenience.
| ouId required | string The unit_id to activate for the current session |
{- "ouId": "string"
}Lists members of the current organization. Results are organization-scoped.
[- {
- "org_member_id": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "role": "string"
}
]| first_name required | string |
| last_name required | string |
| email required | string <email> |
| password required | string <password> |
| role required | string |
{- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "password": "pa$$word",
- "role": "string"
}{- "org_member_id": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "role": "string"
}| id required | string |
| first_name | string |
| last_name | string |
string <email> | |
| role | string |
{- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "role": "string"
}Set a member's Slack user identifier directly, bypassing the email-based sync. Use this when a member's kenbun email differs from their Slack workspace email. Admin role required. Tenant isolation is enforced — admins cannot set a Slack identifier for a member in a different organization.
| org_member_id required | string Identifier of the target member. |
| slack_user_id required | string^U[A-Z0-9]+$ Slack user identifier. Must begin with U followed by uppercase letters and digits. |
{- "slack_user_id": "U012AB3CD"
}Lists members assigned to the specified OU with OU-scoped roles. Requires caller to be a member of that OU.
| id required | string OU ID (unit_id) |
[- {
- "org_member_id": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "user@example.com",
- "role": "Owner"
}
]Assigns a member to the specified OU with a role. Idempotent. Requires OU role Owner or Operator.
| id required | string OU ID (unit_id) |
| org_member_id required | string |
| role | string Default: "Viewer" Enum: "Owner" "Operator" "Viewer" |
{- "org_member_id": "string",
- "role": "Owner"
}| id required | string |
| memberId required | string |
| role required | string Enum: "Owner" "Operator" "Viewer" |
{- "role": "Owner"
}Returns whether rate limiting is enabled, the mode, keying policy, computed key for this caller, and numeric limits.
{- "enabled": true,
- "mode": "observe",
- "key_by": "org",
- "key": "string",
- "rps": 0,
- "burst": 0,
- "stage": "string"
}Returns the calling user's notification preferences for the active Organizational Unit. Always lists all four canonical event types; any event type without a stored row defaults to muted=false. The slack_user_id field is masked for display (e.g. "U12•••XYZ") or null when the user is not yet mapped.
{- "notifications_enabled": true,
- "slack_user_id": "string",
- "prefs": [
- {
- "event_type": "milestone.entered",
- "muted": true
}
]
}Update the master toggle, the per-event-type mute settings, or both. Both top-level fields are optional;
omitted fields are left unchanged. Each entry in prefs must reference a canonical event_type, otherwise the request is rejected with 400.
| notifications_enabled | boolean Master toggle. |
Array of objects |
{- "notifications_enabled": true,
- "prefs": [
- {
- "event_type": "milestone.entered",
- "muted": true
}
]
}{- "status": "ok"
}Creates an org-owned Service Account: a client_id / client_secret pair
used with HTTP Basic auth for unattended automation (for example n8n).
Pass scopes to make the credential least-privilege: when scopes are set,
the credential can only reach routes covered by those scopes (every other
token-gated route returns 403 insufficient_scope). Omit scopes (or send
an empty list) for a legacy full-access credential. Fetch the available
scopes from GET /settings/token-scopes.
| name required | string <= 50 characters Human-friendly label for this credential (1–50 chars) |
| scopes | Array of strings Optional least-privilege scopes (read: |
{- "name": "string",
- "scopes": [
- "write:scoring",
- "read:scoring"
]
}{- "secret_id": "string",
- "client_id": "string",
- "client_secret": "string",
- "name": "string",
- "scopes": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z"
}SSE stream of score updates, surge events, and newly ingested events.
OU scoping:
ouId as a query param to receive only events for that OU.activeOu cookie.Note: This endpoint is currently unauthenticated.
| ouId | string Organization Unit (OU) ID to scope the stream. |
event: score_update data: {"lead_id":"ld_123","delta":5}
Returns current-month event usage, billing limits, and overage information for the organization.
{- "monthly_events": 0,
- "free_tier_limit": 0,
- "overage_rate_cents": 0,
- "overage_unit": 0,
- "overage": true,
- "billing_cycle_start": "2019-08-24T14:15:22Z",
- "billing_cycle_end": "2019-08-24T14:15:22Z",
- "org_id": "string",
- "org_name": "string",
- "subscription_status": "active",
- "trial_ends_at": "2019-08-24T14:15:22Z"
}Receives subscription lifecycle events from kenbun's billing provider. This endpoint is called automatically by the billing system — not by API clients. Processes subscription.created, subscription.updated, subscriptionItem.freeTrialEnding, and payment_attempt events. All requests are authenticated using a signed-payload signature, which kenbun verifies on every incoming request.
| type | string Event type (e.g., subscription.created, subscription.updated) |
| data | object Event payload (varies by type) |
{- "type": "string",
- "data": { }
}{- "received": true
}Returns aggregated surge metrics for the specified organization unit within a time range. Requires authentication and an active organization; OU membership is enforced.
| ouId required | string Organization Unit ID to scope the results. |
| start required | string <date-time> Start of the time range (RFC3339). |
| end required | string <date-time> End of the time range (RFC3339). |
| interval | string Enum: "15m" "1h" "1d" Aggregation interval. Defaults to |
| channel | string Optional channel filter (e.g., 'web', 'email', 'slack'). |
{- "ou_id": "string",
- "start": "2019-08-24T14:15:22Z",
- "end": "2019-08-24T14:15:22Z",
- "interval": "15m",
- "totals": {
- "surges": 0,
- "unique_leads": 0
}, - "peak": {
- "ts": "2019-08-24T14:15:22Z",
- "count": 0
}, - "channels": [
- {
- "channel": "string",
- "count": 0
}
], - "series": [
- {
- "ts": "2019-08-24T14:15:22Z",
- "total": 0,
- "by_channel": {
- "property1": 0,
- "property2": 0
}
}
]
}Runs account/profile mapping and recalculates profile scores for all leads in the active OU.
{- "leads_processed": 0,
- "accounts_linked": 0,
- "accounts_created": 0,
- "profile_scores_updated": 0
}Adds or corrects the accountId in event metadata by joining each event's lead to its associated account within the active OU. No-op for events that already have accountId.
{- "events_scanned": 0,
- "events_updated": 0,
- "leads_without_account": 0
}Reads all leads in the active OU and applies the currently configured profile field mappings to populate lead metadata. Respects each mapping's update behavior setting.
{- "leads_processed": 0,
- "leads_updated": 0
}Returns recent audit log entries for the active OU with optional filters and sorting.
| limit | integer [ 1 .. 500 ] Max rows to return (default 100; max 500) |
| offset | integer >= 0 Number of rows to skip (for pagination) |
| search | string Search across user_email, path, and IP address |
| method | string Enum: "GET" "POST" "PUT" "PATCH" "DELETE" Filter by HTTP method |
| status_min | integer Minimum status code (inclusive) |
| status_max | integer Maximum status code (inclusive) |
| user_email | string Filter by exact user email |
| sort_by | string Default: "created_at" Enum: "created_at" "user_email" "method" "path" "status" "ip" "latency_ms" Column to sort by |
| sort_order | string Default: "DESC" Enum: "ASC" "DESC" Sort order (ascending or descending) |
{- "items": [
- {
- "id": "string",
- "org_id": "string",
- "unit_id": "string",
- "user_sub": "string",
- "user_email": "string",
- "method": "string",
- "path": "string",
- "status": 0,
- "ip": "string",
- "user_agent": "string",
- "latency_ms": 0,
- "request_id": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
], - "limit": 0,
- "offset": 0
}Returns whether Slack app credentials are configured for the current organization.
{- "configured": true,
- "client_id": "string",
- "has_signing_secret": true,
- "has_oauth_client_secret": true
}Saves the Slack app credentials for the current organization. The Client Secret (oauth_client_secret) is used when connecting your workspace; the Signing Secret (signing_secret) verifies interactive button clicks. Both come from your Slack app's Basic Information > App Credentials page and are different values. Send an empty string (or omit a field) to keep its previously saved value.
| client_id required | string |
| signing_secret | string Verifies interactive button clicks. Empty keeps the saved value. |
| oauth_client_secret | string Used in the workspace connection flow. Empty keeps the saved value. |
| client_secret | string Deprecated alias for signing_secret, kept for older clients. |
{- "client_id": "string",
- "signing_secret": "string",
- "oauth_client_secret": "string",
- "client_secret": "string"
}Fetches a public website and returns proposed Page View scoring rules based on URL pattern categorization. Tries sitemap.xml first, falls back to homepage crawl + one level deep. Returns one proposed rule per intent category found on the site. Used by the Quick Start wizard to bootstrap scoring configuration.
| url required | string Public website URL to analyze. Scheme is added automatically if omitted. |
{
}{- "domain": "example.com",
- "fetched_url": "string",
- "page_count": 0,
- "skipped_urls": 0,
- "source": "sitemap",
- "categories": [
- {
- "category": "High Intent - Pricing",
- "pattern": "pricing",
- "urls": [
- "string"
], - "suggested_weight": 0,
- "icon": "string"
}
], - "proposed_rules": [
- {
- "event_type": "Page View",
- "weight": 0,
- "label": "string",
- "icon": "string",
- "attribute_filters": [
- {
- "field": "path",
- "operator": "contains",
- "value": "pricing"
}
]
}
]
}Returns the full least-privilege scope taxonomy (read:
{- "scopes": [
- {
- "value": "write:scoring",
- "label": "Write Scoring",
- "description": "Create, update, and delete scoring rules, levels, mappings, filters, and score reads",
- "domain": "scoring",
- "access": "read"
}
]
}Generate a new Personal Access Token (PAT) for the authenticated user.
PATs authenticate API and MCP requests as the user with their permissions.
The full token value (beginning with kbn_pat_) is returned once in this response.
Store it securely — it cannot be retrieved again.
| name required | string [ 1 .. 100 ] characters Descriptive label for the token (1–100 characters) |
| scopes | Array of strings Optional least-privilege scopes (read: |
| expires_at | string or null <date-time> Optional expiry date and time in RFC 3339 format. If omitted, the token does not expire. Must be a future timestamp. |
{- "name": "MCP Server - Laptop",
- "scopes": [
- "write:scoring",
- "read:scoring"
], - "expires_at": "2027-01-01T00:00:00Z"
}{- "id": "string",
- "name": "string",
- "token": "kbn_pat_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
- "token_prefix": "kbn_pat_a1b2c3d4",
- "scopes": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z"
}Returns all Personal Access Tokens for the authenticated user. Token values are never included in this response — only metadata for managing tokens.
[- {
- "id": "string",
- "name": "string",
- "token_prefix": "kbn_pat_a1b2c3d4",
- "scopes": [
- "string"
], - "created_at": "2019-08-24T14:15:22Z",
- "last_used_at": "2019-08-24T14:15:22Z",
- "expires_at": "2019-08-24T14:15:22Z",
- "revoked_at": "2019-08-24T14:15:22Z"
}
]Revokes a Personal Access Token. Once revoked, any system using that token will immediately lose access. This action cannot be undone. Users can only revoke their own tokens.
| id required | string The token ID (from POST /settings/tokens or GET /settings/tokens) |
Returns organization event types with hidden flag and per-OU event counts.
[- {
- "name": "string",
- "hidden": true,
- "max_processing_scope": "observe",
- "ou_event_count": 0,
- "protected": true,
- "deletable": true
}
]| eventType required | string |
| hidden | boolean |
| max_processing_scope | string Enum: "observe" "decide" "activate" Ceiling for event processing in this OU; does not imply execution. |
{- "hidden": true,
- "max_processing_scope": "observe"
}Returns whether the active OU is connected to a Slack workspace and basic metadata.
{- "connected": true,
- "team_name": "string",
- "team_id": "string",
- "bot_user_id": "string",
- "updated_at": "2019-08-24T14:15:22Z",
- "default_target_kind": "channel",
- "default_target_id": "string",
- "authorize_url": "string"
}Proxies Slack conversations.list and returns the upstream response. Requires the OU to be connected to Slack.
| types | string Example: types=public_channel,private_channel |
{- "ok": true,
- "channels": [
- {
- "id": "string",
- "name": "string",
- "is_private": true
}
], - "response_metadata": {
- "next_cursor": "string"
}
}Proxies Slack users.list and returns the upstream response. Requires the OU to be connected to Slack.
{- "ok": true,
- "members": [
- {
- "id": "string",
- "name": "string"
}
], - "response_metadata": {
- "next_cursor": "string"
}
}Persists the default Slack target (channel or user) for notifications.
| kind required | string Enum: "channel" "user" |
| id required | string |
{- "kind": "channel",
- "id": "string"
}Sends a simple test message to the provided channel or user for the active OU. If kind=user, opens a DM first.
| kind required | string Enum: "channel" "user" |
| id required | string |
| text | string Optional custom message |
{- "kind": "channel",
- "id": "string",
- "text": "string"
}{- "message": "string"
}Returns whether the OU has opted in to a Slack post when a scoring ruleset is published (ADR-0050 Phase 1.5.4). Default false. When the OU is not connected to Slack, returns { enabled: false } so the UI can render a consistent empty state.
{- "enabled": true
}Persists the per-OU opt-in for posting to Slack on ruleset publish. Returns 404 when the OU has no Slack connection — connect Slack first.
| enabled required | boolean |
{- "enabled": true
}{- "enabled": true
}Returns the active OU's conversion definition, cold-start calibration state, and (when a HubSpot connection exists with a recorded sync time) HubSpot sync freshness information.
The hubspot_sync_freshness key is omitted entirely when no HubSpot
connection is configured or when last_sync_at has never been recorded.
cold_start_state.eligible is true when conversions_observed >= 30, which
is the minimum signal required for the calibration engine to run.
{- "definition": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "org_id": "string",
- "unit_id": "5becc822-b69e-4e66-a762-ad8e868dcab6",
- "conversion_type": "closed_won",
- "event_type_id": 0,
- "deal_stages": [
- "string"
], - "engagement_persistence_days": 7,
- "lookback_days": 30,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}, - "cold_start_state": {
- "leads_scored": 0,
- "conversions_observed": 0,
- "conversions_required": 30,
- "eligible": true
}, - "hubspot_sync_freshness": {
- "last_synced_at": "2019-08-24T14:15:22Z",
- "hours_since_sync": 0.1,
- "stale": true
}
}Creates or replaces the active OU's conversion definition. Only one definition per OU is allowed; calling PUT again replaces the existing row.
Validation rules:
conversion_type must be one of: closed_won, deal_stage_reached,
event_type_match, engagement_persistence, manual_label, sales_disposition.sales_disposition counts leads marked accepted_sql (Qualified) or
closed_won within the look-back window (closed-loop disposition feedback, ADR-0066).closed_won / deal_stage_reached: deal_stages must be non-empty.
If HubSpot pipeline stages have been synced, each stage ID is validated against
the known list. If no stages have been synced yet, validation is skipped.event_type_match: event_type_id must reference an existing event type
for this organisation.engagement_persistence_days must be 7–60 (default 14).lookback_days must be 30–365 (default 90).Every successful PUT discards the OU's cached calibration runs so future reads recompute under the new definition.
| conversion_type required | string Enum: "closed_won" "deal_stage_reached" "event_type_match" "engagement_persistence" "manual_label" |
| deal_stages | Array of strings Required for closed_won / deal_stage_reached. |
| event_type_id | integer Required for event_type_match. Must reference an existing event type for this organisation. |
| engagement_persistence_days | integer [ 7 .. 60 ] Default: 14 Inactivity window in days (engagement_persistence type only). |
| lookback_days | integer [ 30 .. 365 ] Default: 90 Historical window for conversion counting. |
{- "conversion_type": "closed_won",
- "deal_stages": [
- "string"
], - "event_type_id": 0,
- "engagement_persistence_days": 14,
- "lookback_days": 90
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "org_id": "string",
- "unit_id": "5becc822-b69e-4e66-a762-ad8e868dcab6",
- "conversion_type": "closed_won",
- "event_type_id": 0,
- "deal_stages": [
- "string"
], - "engagement_persistence_days": 7,
- "lookback_days": 30,
- "created_by": "ee824cad-d7a6-4f48-87dc-e8461a9201c4",
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}Returns the OU-scoped session-based surge multiplier policy for the active OU. If none exists, returns Standard defaults.
{- "org_id": "string",
- "unit_id": "string",
- "mode": "off",
- "bands": [
- {
- "start_min": 0,
- "end_min": 1,
- "multiplier": 1
}
], - "idle_reset_seconds": 1,
- "multiplier_cap": 1
}Creates or updates the session-based surge multiplier policy for the active OU.
| mode | string Enum: "off" "standard" "gentle" "custom" |
Array of objects | |
| idle_reset_seconds | integer >= 1 |
| multiplier_cap | number [ 1 .. 3 ] |
{- "mode": "off",
- "bands": [
- {
- "start_min": 0,
- "end_min": 1,
- "multiplier": 1
}
], - "idle_reset_seconds": 1,
- "multiplier_cap": 1
}Returns the per-OU settings for the weekly "leads awaiting disposition" Slack reminder and the calibration payoff message. When no settings have been saved, the enabled defaults are returned.
{- "enabled": true,
- "payoff_enabled": true,
- "cadence_days": 1,
- "min_age_days": 365,
- "min_score": 0,
- "max_leads_per_owner": 1
}Creates or updates the disposition nudge settings for the active Organizational Unit. Every field is optional; only the fields you send are changed, and the rest keep their current values.
| enabled | boolean Turn the awaiting-disposition reminder on or off. |
| payoff_enabled | boolean Turn the calibration payoff and readiness messages on or off. |
| cadence_days | integer [ 1 .. 365 ] How often the reminder is sent, in days. |
| min_age_days | integer [ 0 .. 365 ] How long a lead must have had an owner before it appears. |
| min_score | integer >= 0 Only leads scoring at or above this value are included. |
| max_leads_per_owner | integer [ 1 .. 50 ] The most leads listed in one owner's reminder. |
{- "enabled": true,
- "cadence_days": 7,
- "min_age_days": 3,
- "min_score": 25,
- "max_leads_per_owner": 8
}{- "enabled": true,
- "payoff_enabled": true,
- "cadence_days": 1,
- "min_age_days": 365,
- "min_score": 0,
- "max_leads_per_owner": 1
}Returns the total number of confirmed members in the active Organizational Unit and how many of them have a Slack user identifier on file. Used by the trigger configuration UI to render the "X of Y mapped" status pill next to the "Notify the Lead Owner" option. Available to any authenticated member of the active Organizational Unit (admin role not required).
{- "total": 0,
- "mapped": 0
}Looks up every confirmed member in the active Organizational Unit by email in the connected Slack workspace and stores any matches. Members already mapped are skipped. The endpoint blocks until the sync completes and returns a summary. Admin role required.
{- "members_total": 0,
- "matched": 0,
- "no_match": 0,
- "errors": 0
}Unauthenticated endpoints that serve publicly shareable content. These endpoints do not require an API key or session cookie.
Returns the score explanation for a previously shared link. No authentication is required. Returns 410 if the link has expired, 404 if the token does not exist.
| token required | string Share token returned by POST /leads/{id}/share-explain |
{- "lead_name": "string",
- "score": 0,
- "top_drivers": [
- {
- "rule_id": "string",
- "event_type": "string",
- "count": 0,
- "weight": 0,
- "total": 0,
- "filter_label": "string",
- "half_life_days": 0,
- "most_recent_at": "2019-08-24T14:15:22Z"
}
], - "snapshot": [
- {
- "event_type": "string",
- "weight": 0
}
], - "decay_applied": 0,
- "range_hours": 0
}HubSpot integration endpoints for connecting kenbun to your HubSpot CRM, syncing contacts and deals, managing engagement history preferences, and monitoring sync health. All endpoints require an authenticated session and an active Organizational Unit.
Updates which HubSpot engagement types (emails, calls, meetings, notes, tasks) are synced for the active Organizational Unit. Optionally marks the connection as including engagement history imports.
Changes take effect for future syncs. To trigger an immediate import of past
engagement activity, use POST /settings/ou/hubspot/backfill with
include_engagement_history: true.
| engagement_types required | Array of strings Items Enum: "emails" "calls" "meetings" "notes" "tasks" Engagement types to sync going forward. Pass an empty array to disable all types. |
| include_engagement_history | boolean When true, marks that engagement history imports are enabled for this connection. |
{- "engagement_types": [
- "emails",
- "calls",
- "meetings"
], - "include_engagement_history": false
}{- "engagement_types": [
- "string"
], - "include_engagement_history": true
}Returns the current engagement history sync configuration and status for the active Organizational Unit. Use this to check which engagement types are enabled and when the last engagement import completed.
{- "engagement_types": [
- "emails",
- "calls",
- "meetings"
], - "include_engagement_history": true,
- "engagement_history_synced": true,
- "engagement_history_synced_at": "2019-08-24T14:15:22Z"
}Returns the disposition writeback configuration for the active Organizational Unit. Disposition writeback updates a HubSpot contact's lifecycle stage and lead status when a rep logs a sales outcome on the matched lead. Off by default; when enabled, comes seeded with sensible default mappings.
{- "enabled": true,
- "mappings": {
- "property1": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}, - "property2": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}
}
}Enables or disables disposition writeback and sets the per-disposition mapping to HubSpot lifecycle stage and lead status. Mapped values are accepted as-is; the writeback worker only ever moves a contact's lifecycle stage forward and never reverts a property when a disposition is cleared.
| enabled | boolean Whether outbound disposition writeback is active for this OU. |
object Map keyed by kenbun disposition value to its HubSpot property target. |
{- "enabled": true,
- "mappings": {
- "property1": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}, - "property2": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}
}
}Returns the connected HubSpot portal's actual lifecycle stage and lead status option lists (including custom stages), alongside the current configuration and the default mapping. Use this to render the writeback mapping dropdowns so they match the customer's portal rather than guessing values.
{- "lifecycle_stages": [
- {
- "label": "string",
- "value": "string",
- "displayOrder": 0,
- "hidden": true
}
], - "lead_statuses": [
- {
- "label": "string",
- "value": "string",
- "displayOrder": 0,
- "hidden": true
}
], - "config": {
- "enabled": true,
- "mappings": {
- "property1": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}, - "property2": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}
}
}, - "defaults": {
- "property1": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}, - "property2": {
- "lifecycle_stage": "salesqualifiedlead",
- "lead_status": "OPEN"
}
}
}Queues a background import of historical HubSpot data for the active Organizational Unit. Supports importing contacts, engagement history, or both in a single request.
Backwards compatibility: Sending no request body (or omitting include_contacts)
defaults to importing contacts only — matching the original behavior of this endpoint.
All queued jobs run in the background. For large datasets (10,000+ records), imports may take several hours. Check sync status to monitor progress.
| include_contacts | boolean Whether to import historical contacts. Defaults to |
| include_engagement_history | boolean Whether to import historical engagement activity (emails, calls, meetings, notes, tasks). |
| engagement_types | Array of strings Items Enum: "emails" "calls" "meetings" "notes" "tasks" Which engagement types to import. Only used when |
| force | boolean When |
{- "include_contacts": true,
- "include_engagement_history": true,
- "engagement_types": [
- "emails",
- "calls",
- "meetings"
], - "force": false
}{- "status": "queued",
- "message": "Historical import started",
- "queued": [
- "contacts",
- "engagements"
]
}Returns the current HubSpot connection state for the active Organizational Unit, including which OAuth app is brokering the connection.
The app_mode field tells callers whether the connection is using the
kenbun-managed shared OAuth app (the default for most customers) or a
customer-supplied OAuth app (Bring Your Own App, enterprise opt-in):
shared — connection uses the kenbun-managed OAuth app. No customer
credentials are stored on the connection row.byo — connection uses customer-supplied OAuth credentials saved via
POST /settings/ou/hubspot/credentials.unconfigured — neither shared OAuth credentials nor customer-supplied
credentials are available. The Connect button is not usable until one
source is configured.{- "connected": true,
- "app_mode": "shared",
- "has_credentials": true,
- "portal_id": "string",
- "last_synced_at": "2019-08-24T14:15:22Z"
}Builds the HubSpot authorization URL for the active Organizational Unit and returns it to the caller, who should redirect the browser to that URL. After the user consents in HubSpot, HubSpot redirects back to the kenbun OAuth callback to complete the handshake.
The endpoint uses whichever OAuth app is configured for the active OU:
app_mode: byo), the URL is built using those credentials.app_mode: shared).If neither source is available, the endpoint returns 412 Precondition Failed so the UI can prompt the customer to either contact support or configure Bring Your Own App credentials.
{- "app_mode": "shared"
}Stores Client ID and Client Secret for a customer-owned HubSpot OAuth
app on the active Organizational Unit's connection row. After saving,
subsequent calls to POST /settings/ou/hubspot/oauth/initiate build
the authorization URL using these credentials, putting the connection
into app_mode: byo.
Most customers do not need this endpoint. It exists for enterprise teams that require dedicated rate limits, customer-branded consent screens, or self-hosted compliance posture. The default Connect HubSpot flow uses the kenbun-managed shared app and does not require any credentials.
| client_id required | string HubSpot OAuth app Client ID from your HubSpot Developer Account. |
| client_secret required | string HubSpot OAuth app Client Secret. Stored encrypted at rest. |
{- "client_id": "string",
- "client_secret": "string"
}{- "status": "saved",
- "app_mode": "byo"
}Removes the customer-supplied Client ID and Client Secret from the
active Organizational Unit's HubSpot connection, reverting it to the
kenbun-managed shared OAuth app. After this call, the connection's
app_mode becomes shared and the customer must re-authorize via
POST /settings/ou/hubspot/oauth/initiate so HubSpot issues new
tokens against the shared app.
Previously synced contacts, deals, engagement history, and scores are preserved. Only the OAuth app routing changes.
{- "status": "cleared",
- "app_mode": "shared"
}Setup status endpoint used by the guided setup checklist on the Overview dashboard. Completion is auto-detected from real OU-scoped data — no manual state to manage.
Returns the completion status of each guided setup step for the active Organizational Unit. Completion is auto-detected from real data — no separate flag to set. Steps: connect an integration, create a scoring rule, set up engagement levels. Also accessible at the legacy path /onboarding-status (same handler, same response).
{- "steps": [
- {
- "id": "connect_integration",
- "label": "Connect an integration",
- "completed": true,
- "action_path": "/settings/integrations"
}, - {
- "id": "create_scoring_rules",
- "label": "Create your first scoring rule",
- "completed": false,
- "action_path": "/configure/scoring/engagement"
}, - {
- "id": "set_engagement_levels",
- "label": "Set up engagement levels",
- "completed": false,
- "action_path": "/configure/levels/engagement"
}
], - "all_complete": false,
- "completed_count": 1,
- "total_count": 3
}Phase 2 Feature (ABM Required)
Account-based marketing endpoints require abm_enabled: true in organization scoring settings.
These endpoints return 403 Forbidden if ABM is not enabled for your organization.
Contact your account manager for access.
Returns the first account matching the provided domain or company name. Domain is checked first; name is used as a fallback when domain is not provided or yields no match. Returns null when no matching account is found. OU-scoped to the active Organizational Unit.
| domain | string Domain to search for (e.g., acme.com). Checked before name. |
| name | string Exact company name to search for. Used as fallback when domain is not provided or returns no match. |
{- "account_id": "acct_abc123",
- "name": "Acme Corporation",
- "domain": "acme.com"
}Returns accounts whose names fuzzy-match the given query, ranked by similarity score. Uses a combination of string similarity algorithms (Jaro-Winkler and Token Set Ratio) to find close matches. Returns up to 5 results above a 0.6 similarity threshold. OU-scoped to the active Organizational Unit.
| name required | string Company name to search for (e.g., "Salesforce LLC"). |
{- "matches": [
- {
- "account_id": "acct_abc123",
- "name": "Salesforce",
- "score": 0.92
}, - {
- "account_id": "acct_def456",
- "name": "Salesforce.com Inc",
- "score": 0.78
}
]
}Returns a list of accounts for the active OU by aggregating leads joined with accounts and lead scores. Fields include top_score (max lead score within the account), leads_count, owner_name (from the top-scoring lead), and surging (true if any lead in the account surged in the last 24 hours).
| page | integer >= 1 Default: 1 |
| limit | integer [ 1 .. 500 ] Default: 50 |
| range | string Enum: "all" "today" "yesterday" "last_24h" "last_7d" "last_30d" "last_90d" Filter accounts by created_at using a named range. |
| search | string Search by account name or domain. |
| intent_level | string Enum: "all" "low" "medium" "high" Filter by intent level. |
| sort_by | string Enum: "name" "score" "engagement" "last_activity" "owner_name" Field to sort by. |
| sort_order | string Default: "desc" Enum: "asc" "desc" Sort direction. |
| account_level | string Filter by account level name (e.g., "Target", "Qualified"). |
| min_score | integer Minimum account score. |
| owner_name | string Filter by owner display name (partial match, case-insensitive). |
| has_leads | boolean When true, return only accounts with at least one associated lead. |
| include_disqualified | boolean Default: false When true, include disqualified leads in account aggregation. Defaults to false (qualified-only). |
{- "accounts": [
- {
- "account_id": "acc_123",
- "org_id": "org_1",
- "domain": "example.com",
- "name": "Example Inc",
- "created_at": "2024-01-01T00:00:00Z",
- "account_level": "High",
- "top_score": 612,
- "leads_count": 4,
- "owner_name": "Alex Rivera",
- "surging": true
}
], - "total": 1,
- "page": 1,
- "limit": 25
}Unlinks leads in the active OU from the given accounts and deletes account records that are no longer referenced by any lead.
| ids | Array of strings |
{- "ids": [
- "string"
]
}{- "account_id": "string",
- "org_id": "string",
- "domain": "string",
- "name": "string",
- "metadata": { },
- "top_score": 0,
- "leads": [
- {
- "lead_id": "string",
- "score": 0,
- "owner_name": "string",
- "last_event_at": "2019-08-24T14:15:22Z"
}
], - "recent_events": [
- {
- "id": "string",
- "lead_id": "string",
- "event_type": "string",
- "source": "string",
- "timestamp": "2019-08-24T14:15:22Z"
}
]
}Merges metadata into an account and optionally updates top-level name and domain fields. This is a partial update -- existing metadata keys not included in the request are preserved. OU-scoped to the active Organizational Unit.
| id required | string Account ID |
| name | string Update the account's company name. |
| domain | string Update the account's primary email domain. |
object Key-value pairs to merge into the account's metadata. Setting a key to null removes it. |
{- "name": "Acme Corporation",
- "domain": "acme.com",
- "metadata": {
- "industry": "Technology",
- "employees": 2500,
}
}Merges the source account (identified by {id}) into a target account. All leads and deals belonging to the source account are reassigned to the target account. The source account is then archived. This action is permanent and cannot be undone. Both accounts must belong to the same Organizational Unit. The merge is recorded in the audit log.
| id required | string The source account ID to be archived |
| target_id required | string The ID of the target account that will receive all leads and deals |
{- "target_id": "acct_def456"
}{- "leads_reassigned": 8,
- "deals_reassigned": 3
}Returns the aggregated Top-3 scoring factors across all qualified contacts associated with the account (the buying committee). Per-factor aggregation uses MAX contribution so the most-engaged contact's signal carries through rather than being diluted by less-engaged committee members.
DQ handling: disqualified contacts are excluded. When more than 50% of contacts
are disqualified, mostly_disqualified is set to true in the response.
When the account has no contacts (or all are disqualified), summary is
"No qualified contacts in this account." and factors is empty.
| accountID required | string The account ID |
{- "account_id": "string",
- "summary": "string",
- "factors": [
- {
- "dimension": "engagement",
- "rule_name": "string",
- "contribution": 0,
- "matched_value": "string",
- "event_count": 0,
- "days_since_fresh": 0,
- "phrase": "string"
}
], - "contributing_lead_count": 0,
- "total_contact_count": 0,
- "mostly_disqualified": true
}Evaluates an account scoring rule against a specific account without modifying production scores.
| lead_id required | string The lead whose account to test against |
required | object The account rule configuration to evaluate |
{- "lead_id": "string",
- "rule": {
- "account_property": "string",
- "condition": "equals",
- "comparison_value": "string",
- "weight": 0
}
}{- "matches": true,
- "reason": "string",
- "current_score": 0,
- "projected_score": 0,
- "matching_value": "string"
}[- {
- "id": "string",
- "org_id": "string",
- "name": "string",
- "enabled": true,
- "is_primary": true,
- "score_floor_pct": 0.5,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| name required | string |
| enabled | boolean |
| is_primary | boolean Default: false |
{- "name": "string",
- "enabled": true,
- "is_primary": false
}{- "id": "string",
- "org_id": "string",
- "name": "string",
- "enabled": true,
- "is_primary": true,
- "score_floor_pct": 0.5,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string |
| name required | string |
| enabled | boolean |
| is_primary | boolean Default: false |
{- "name": "string",
- "enabled": true,
- "is_primary": false
}| id required | string |
[- {
- "id": "string",
- "ruleset_id": "string",
- "account_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "half_life_days": 1,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}, - "ui_order": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}
]| id required | string |
| account_property required | string |
| condition required | string |
| comparison_value required | string |
| weight required | integer |
| ui_order | integer |
| rule_type | string Enum: "add_points" "hard_disqualify" Defaults to add_points when omitted. |
object or null Compound AND/OR condition group. Only valid when rule_type is hard_disqualify. |
{- "account_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "ui_order": 0,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}
}{- "id": "string",
- "ruleset_id": "string",
- "account_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "half_life_days": 1,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}, - "ui_order": 0,
- "created_at": "2019-08-24T14:15:22Z",
- "updated_at": "2019-08-24T14:15:22Z"
}| id required | string |
| ruleId required | string |
| account_property | string |
| condition | string |
| comparison_value | string |
| weight | integer |
| ui_order | integer |
| rule_type | string Enum: "add_points" "hard_disqualify" |
object or null Compound AND/OR condition group. Only valid when rule_type is hard_disqualify. |
{- "account_property": "string",
- "condition": "string",
- "comparison_value": "string",
- "weight": 0,
- "ui_order": 0,
- "rule_type": "add_points",
- "conditions": {
- "op": "AND",
- "children": [
- {
- "property": "string",
- "condition": "string",
- "value": "string"
}
]
}
}Returns the audit log of all account mapping changes — creates, updates, and deletes — for the active OU.
[- {
- "id": "string",
- "mapping_type": "string",
- "action": "created",
- "actor": "string",
- "old_value": { },
- "new_value": { },
- "created_at": "2019-08-24T14:15:22Z"
}
]| source_field required | string |
| account_property required | string |
| update_behavior | string Default: "always_latest" Enum: "always_latest" "set_once" "append" Controls how this field is updated when new events arrive. For append, last 10 values are stored in " |
{- "source_field": "string",
- "account_property": "string",
- "update_behavior": "always_latest"
}{- "id": "string",
- "source_field": "string",
- "account_property": "string",
- "update_behavior": "always_latest",
- "created_at": "2019-08-24T14:15:22Z"
}| id required | string |
| source_field required | string |
| account_property required | string |
| update_behavior | string Default: "always_latest" Enum: "always_latest" "set_once" "append" Controls how this field is updated when new events arrive. For append, last 10 values are stored in " |
{- "source_field": "string",
- "account_property": "string",
- "update_behavior": "always_latest"
}Returns the buying committee members for an account, including their roles, engagement scores, and activity metrics.
| accountId required | string <uuid> Account identifier |
[- {
- "id": "string",
- "lead_id": "string",
- "account_id": "string",
- "role": "decision_maker",
- "engagement_score": 0,
- "activity_count": 0,
- "first_engagement_at": "2019-08-24T14:15:22Z",
- "last_engagement_at": "2019-08-24T14:15:22Z",
- "is_manual": true,
- "notes": "string",
- "best_alias": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "title": "string",
- "company_name": "string",
- "engagement_level": "string",
- "profile_level": "string",
- "owner_name": "string"
}
]Manually add a lead to an account's buying committee with a specific role. Manually added members are preserved during automatic role refresh.
| accountId required | string <uuid> |
| lead_id required | string ID of the lead to add |
| role required | string Enum: "decision_maker" "influencer" "champion" "kol" "other" Committee role |
| notes | string Optional notes about this member |
{- "lead_id": "string",
- "role": "decision_maker",
- "notes": "string"
}{- "id": "string",
- "lead_id": "string",
- "account_id": "string",
- "role": "decision_maker",
- "engagement_score": 0,
- "activity_count": 0,
- "first_engagement_at": "2019-08-24T14:15:22Z",
- "last_engagement_at": "2019-08-24T14:15:22Z",
- "is_manual": true,
- "notes": "string",
- "best_alias": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "title": "string",
- "company_name": "string",
- "engagement_level": "string",
- "profile_level": "string",
- "owner_name": "string"
}Re-runs role inference for auto-detected members based on current engagement data. Manually assigned roles are preserved.
| accountId required | string <uuid> |
[- {
- "id": "string",
- "lead_id": "string",
- "account_id": "string",
- "role": "decision_maker",
- "engagement_score": 0,
- "activity_count": 0,
- "first_engagement_at": "2019-08-24T14:15:22Z",
- "last_engagement_at": "2019-08-24T14:15:22Z",
- "is_manual": true,
- "notes": "string",
- "best_alias": "string",
- "first_name": "string",
- "last_name": "string",
- "email": "string",
- "title": "string",
- "company_name": "string",
- "engagement_level": "string",
- "profile_level": "string",
- "owner_name": "string"
}
]| accountId required | string <uuid> |
| lead_id required | string |
| role required | string Enum: "decision_maker" "influencer" "champion" "kol" "other" |
| notes | string |
{- "lead_id": "string",
- "role": "decision_maker",
- "notes": "string"
}Activation endpoints help a new workspace get started quickly: generate a realistic sample pipeline to explore the product, then remove it for a clean slate, and fire a single test event to watch a lead's score move in real time. Sample and test data are excluded from billing and usage. All endpoints require an authenticated session and an active Organizational Unit; org_id and unit_id are derived from your account context and are never read from the request body.
Returns whether the active Organizational Unit currently has a generated sample data batch, and whether the OU is "empty" (has no real, non-sample leads or events). The Generate Sample Data action is only offered for an empty OU.
org_id and unit_id are derived from your account context; they are never read from the request body.
{- "batch_exists": false,
- "empty_ou": true,
- "generated_at": null,
- "summary": null
}Generates a complete demo pipeline for the active Organizational Unit:
a populated scoring configuration plus realistic leads, accounts, events,
and a fired milestone. Generation runs in the background and the response
returns immediately with 202 Accepted and a batch_id. Poll
GET /sample-data/status to see the populated summary once generation
finishes.
Generation is only allowed on an empty workspace and is idempotent: if the
OU already has real leads/events, or a sample batch already exists, the
request is rejected with 409 Conflict.
Sample events are recorded with source: sample_data and are excluded from
billing and usage. The request takes no body; org_id and unit_id are derived
from your account context and are never read from the request body.
{- "batch_id": "3f2a9c1e-5b4d-4e2a-9f7c-1a2b3c4d5e6f",
- "status": "generating"
}Removes every row generated by the sample data batch for the active Organizational Unit — both the demo scoring configuration and the demo leads, accounts, and events — returning the OU to a clean slate. The prior primary scoring ruleset (if any) is restored.
Returns the per-category counts of removed records. Returns 404 Not Found
when there is no sample data to remove. The request takes no body; org_id
and unit_id are derived from your account context and are never read from
the request body.
{- "summary": {
- "leads": 6,
- "engagement_events": 42,
- "accounts": 3,
- "scoring_rules": 9,
- "scoring_rulesets": 1
}
}Fires a single synthetic engagement event through real scoring and returns
the score before and after, so you can watch a lead's engagement score move
in real time. If the resulting score crosses an active milestone threshold,
a trigger_fired alert is broadcast to the in-app live feed and
trigger_fired is set in the response.
By default no external notification is sent. Set deliver_to_channel: true
to also send a real test message to the OU's configured Slack channel,
reusing the standard channel-test path. (Microsoft Teams test sends are not
supported yet and return an delivery block with teams_test_unsupported.)
Test events are recorded with source: test and are excluded from billing
and usage. org_id and unit_id are derived from your account context; any
org_id/unit_id keys inside metadata are ignored.
| event_type required | string The event type to fire (e.g. "Form Submit", "Page View"). |
| lead_id | string Optional. The lead to apply the event to. When omitted, score_before and score_after are reported as 0 and no milestone can fire. |
object Optional event metadata (e.g. page path, form id). Any org_id/unit_id keys are ignored — scoping comes from your account context. | |
| deliver_to_channel | boolean Default: false When true, also send a real test message to the OU's configured Slack channel. Defaults to false (in-app alert only). Teams channels are not supported yet. |
{- "event_type": "Form Submit",
- "lead_id": "ld_123",
- "metadata": {
- "form_id": "demo-request"
}
}{- "ok": true,
- "lead_id": "ld_123",
- "event_type": "Form Submit",
- "score_before": 54,
- "score_after": 84,
- "score_delta": 30,
- "trigger_fired": true,
- "trigger_name": "Form Submission + 30 Points",
- "explanation": "Score 84 crossed threshold 30 for trigger \"Form Submission + 30 Points\""
}Evaluates a deal scoring rule against a specific deal without modifying production scores.
| deal_id required | string The deal to test the rule against |
required | object The deal rule configuration to evaluate |
{- "deal_id": "string",
- "rule": {
- "deal_property": "string",
- "condition": "equals",
- "comparison_value": "string",
- "weight": 0
}
}{- "matches": true,
- "reason": "string",
- "current_score": 0,
- "projected_score": 0,
- "matching_value": "string"
}Returns snapshots of the engagement level configuration, saved each time a level is created, updated, or deleted.
| ruleset_id required | string The ruleset ID to retrieve history for |
[- {
- "id": "string",
- "ruleset_id": "string",
- "score_type": "string",
- "snapshot": [
- { }
], - "actor": "string",
- "created_at": "2019-08-24T14:15:22Z"
}
]Replaces the current engagement level configuration with a previously saved snapshot.
| history_id required | string ID of the history entry to revert to |
{- "history_id": "string"
}Returns snapshots of the profile level configuration, saved each time a level is created, updated, or deleted.
| ruleset_id required | string |
[- { }
]Returns snapshots of the account level configuration, saved each time a level is created, updated, or deleted.
| ruleset_id required | string |
[- { }
]| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}{- "stage_id": "st_hot",
- "name": "Hot",
- "min_score": 80,
- "max_score": 1000
}| id required | string |
| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}Lists configured profile levels within the active OU.
[- {
- "level_id": "string",
- "name": "string",
- "min_score": 0,
- "max_score": 0,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}
]| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}{- "level_id": "string",
- "name": "string",
- "min_score": 0,
- "max_score": 0,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}Lists configured account levels within the active OU.
[- {
- "level_id": "string",
- "name": "string",
- "min_score": 0,
- "max_score": 0,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}
]| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}{- "level_id": "string",
- "name": "string",
- "min_score": 0,
- "max_score": 0,
- "org_id": "string",
- "created_by": "string",
- "updated_by": "string"
}| id required | string |
| name required | string |
| min_score required | integer |
| max_score required | integer |
{- "name": "string",
- "min_score": 0,
- "max_score": 0
}Updates scoring settings (organization-wide toggles plus per-OU session idle timeout for the active OU).
| allow_negative_scores | boolean |
| abm_enabled | boolean |
| session_timeout_minutes | integer |
{- "allow_negative_scores": true,
- "abm_enabled": true,
- "session_timeout_minutes": 0
}