Lead Scoring
Automatically score contacts based on email engagement, tag activity, and custom events. Use scores to prioritize outreach, segment audiences, and trigger automations.
Overview
Lead scoring assigns numeric points to contacts based on their behavior — email opens, link clicks, tag assignments, and custom events. Each contact has a leadScore field that accumulates points over time as scoring rules fire.
Use lead scores to:
- Identify your most engaged contacts
- Segment audiences by engagement level (e.g. "hot leads" with score > 50)
- Trigger sequences when contacts reach a score threshold
- Deprioritize inactive contacts with negative scoring
How it works
- Create scoring rules that map events to point values
- When an event occurs (email opened, link clicked, tag added, etc.), the scoring engine checks all active rules
- If a rule matches, points are added to the contact's
leadScoreand a log entry is recorded - You can also manually adjust scores via the API
The scoring rule object
{
"id": 1,
"orgId": 1,
"siteId": null,
"name": "Email opened",
"eventType": "email_open",
"eventFilter": null,
"points": 5,
"maxPerContact": 10,
"active": true,
"createdAt": "2026-03-01T10:00:00.000Z",
"updatedAt": "2026-03-01T10:00:00.000Z"
}Event types
| Event type | Fires when | Example use |
|---|---|---|
email_open | Contact opens an email | +5 points per open |
email_click | Contact clicks a link in an email | +10 points per click |
email_delivered | Email is successfully delivered | +1 point per delivery |
tag_added | A tag is applied to the contact | +20 for "demo-requested" tag |
custom_event | Your app sends a custom event | +15 for "completed_onboarding" |
manual | Score adjusted via API | +/- any amount |
Event filters
Use eventFilter to narrow when a rule fires. The filter is a JSON object where each key must match the event data.
{
"eventType": "email_click",
"eventFilter": { "url": "https://pushmail.dev/pricing" }
}{
"eventType": "tag_added",
"eventFilter": { "tagName": "enterprise" }
}{
"eventType": "email_click",
"eventFilter": { "url": ["https://pushmail.dev/pricing", "https://pushmail.dev/demo"] }
}Rate limiting with maxPerContact
Set maxPerContact to cap how many times a rule can fire for a single contact. This prevents inflated scores from repeated opens or clicks.
maxPerContact: 10— rule stops firing after 10 times for each contactmaxPerContact: null— no limit (rule fires every time)
Create a scoring rule
POST /v1/scoring/rules
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Human-readable rule name (max 200 chars) |
eventTyperequired | string | One of: email_open, email_click, email_delivered, tag_added, custom_event, manual |
pointsrequired | integer | Points to award (can be negative for penalties) |
siteId | integer | Scope rule to a specific site. Omit for org-wide. |
eventFilter | object | JSON filter to narrow event matching |
maxPerContact | integer | Max times this rule fires per contact |
active | boolean | Whether the rule is active (default: true) |
curl -X POST https://pushmail.dev/api/v1/scoring/rules \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Email opened",
"eventType": "email_open",
"points": 5,
"maxPerContact": 10
}'{
"data": {
"id": 1,
"orgId": 1,
"siteId": null,
"name": "Email opened",
"eventType": "email_open",
"eventFilter": null,
"points": 5,
"maxPerContact": 10,
"active": true,
"createdAt": "2026-03-01T10:00:00.000Z"
}
}List scoring rules
GET /v1/scoring/rules
| Parameter | Type | Description |
|---|---|---|
siteId | integer | Filter rules by site (query param). Omit to list all org rules. |
curl "https://pushmail.dev/api/v1/scoring/rules" \
-H "Authorization: Bearer pm_live_YOUR_KEY"Get a scoring rule
GET /v1/scoring/rules/:id
curl https://pushmail.dev/api/v1/scoring/rules/1 \
-H "Authorization: Bearer pm_live_YOUR_KEY"Update a scoring rule
PUT /v1/scoring/rules/:id
| Parameter | Type | Description |
|---|---|---|
name | string | New rule name |
eventType | string | New event type |
eventFilter | object|null | New filter (set to null to clear) |
points | integer | New point value |
maxPerContact | integer|null | New limit (set to null for unlimited) |
active | boolean | Enable or disable the rule |
curl -X PUT https://pushmail.dev/api/v1/scoring/rules/1 \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"points": 10,
"active": false
}'Delete a scoring rule
DELETE /v1/scoring/rules/:id
Existing scoring logs for this rule are preserved (the ruleId reference becomes null).
curl -X DELETE https://pushmail.dev/api/v1/scoring/rules/1 \
-H "Authorization: Bearer pm_live_YOUR_KEY"Get a contact's score
GET /v1/contacts/:id/score
Returns the contact's current lead score and recent scoring log entries.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Number of log entries to return, 1-100 (default: 20) |
curl https://pushmail.dev/api/v1/contacts/42/score \
-H "Authorization: Bearer pm_live_YOUR_KEY"{
"data": {
"contactId": 42,
"email": "jane@example.com",
"leadScore": 45,
"logs": [
{
"id": 10,
"ruleId": 1,
"points": 5,
"reason": "Rule: Email opened",
"createdAt": "2026-03-01T15:00:00.000Z",
"ruleName": "Email opened"
},
{
"id": 9,
"ruleId": 2,
"points": 10,
"reason": "Rule: Link clicked",
"createdAt": "2026-03-01T14:30:00.000Z",
"ruleName": "Link clicked"
},
{
"id": 8,
"ruleId": null,
"points": 25,
"reason": "Sales team qualification",
"createdAt": "2026-03-01T12:00:00.000Z",
"ruleName": null
}
]
}
}Manually adjust a contact's score
POST /v1/contacts/:id/score
Add or subtract points from a contact's score with an optional reason. Useful for sales team adjustments, support escalations, or integrations.
| Parameter | Type | Description |
|---|---|---|
pointsrequired | integer | Points to add (positive) or subtract (negative) |
reason | string | Why this adjustment was made (max 500 chars) |
curl -X POST https://pushmail.dev/api/v1/contacts/42/score \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"points": 25,
"reason": "Attended product demo"
}'{
"data": {
"contactId": 42,
"pointsAdded": 25,
"newScore": 70
}
}curl -X POST https://pushmail.dev/api/v1/contacts/42/score \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"points": -10,
"reason": "Unresponsive for 30 days"
}'Example: typical scoring ruleset
Here is a common set of rules for a B2B SaaS product:
| Rule | Event type | Points | Max per contact |
|---|---|---|---|
| Email delivered | email_delivered | +1 | 50 |
| Email opened | email_open | +5 | 10 |
| Link clicked | email_click | +10 | 20 |
| Clicked pricing page | email_click (filter: pricing URL) | +15 | 3 |
| Tagged as "demo-requested" | tag_added (filter: tagName) | +30 | 1 |
| Completed onboarding | custom_event (filter: name) | +25 | 1 |
| Spam complaint | email_delivered (negative) | -50 | 1 |
Using scores in segments
Combine lead scores with segments to create dynamic audiences based on engagement level:
curl -X POST https://pushmail.dev/api/v1/segments \
-H "Authorization: Bearer pm_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"name": "Hot Leads",
"rules": {
"operator": "AND",
"conditions": [
{ "type": "attribute", "field": "status", "op": "eq", "value": "subscribed" },
{ "type": "attribute", "field": "lead_score", "op": "gte", "value": 50 }
]
}
}'