# Contact Classifier Department Seniority Role Verification (`mambalabs/contact-classifier`) Actor

Contact title in, department and seniority out, plus a check on whether the person still holds the role at that company. Runs a deterministic title ladder with no API key needed. Bring your own LLM key only for the small share of titles the rules cannot place.

- **URL**: https://apify.com/mambalabs/contact-classifier.md
- **Developed by:** [Mamba Labs](https://apify.com/mambalabs) (community)
- **Categories:** Lead generation, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.55 / 1,000 contact classifieds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### 🏷️ What can Contact Classifier do?

Give it a **job title** and it returns the **department** and the **seniority level**. Give it a name
and a company domain too, and it also checks whether that person is **still listed on their
employer's own website**.

It replaces two AI columns in a contact-enrichment workflow with one deterministic call, and it needs
**no API key** to do the classification.

| 📦 What you get | ⚙️ Features and integrations |
|---|---|
| 🏷️ **12 departments and 12 seniority levels**<br>🔢 **`seniority_rank` 1 to 12**, filter with a comparison<br>🧾 **`classification_rule`**, the named rule that fired<br>🧮 **21 flat fields**, `snake_case`, one row per contact | 🧠 **Deterministic first**, no key and no network<br>🔌 **Optional LLM fallback** on your own key<br>🔍 **Position verification** against the employer's site<br>⬇️ **Export** to JSON, CSV, Excel, HTML or XML |

Bought by teams who are paying an LLM to classify job titles and would rather pay a rule table, and
by anyone who needs a decision-maker filter that is auditable.

> 🚫 **This actor does not discover people.** The name and title come from you. It classifies a title
> and, optionally, checks a name against a page the employer published. If you need to find people in
> the first place, use [People Finder](https://apify.com/mambalabs/people-finder).

### 💡 Why use Contact Classifier?

| If you want | Read these fields |
|---|---|
| To route by function | `department` |
| To filter by rank | `seniority`, `seniority_rank` |
| A quick decision-maker gate | `is_decision_maker` |
| To audit the classification | `classification_method`, `classification_rule` |
| Whether the person is still there | `position_status`, `position_evidence_url` |
| To judge that verdict yourself | `peers_found_on_page`, `pages_reached` |

#### 🎯 The rules place about 9 titles in 10 with no AI at all

Measured on 48 real job titles pulled from live company pages and job boards: **92 percent got a
department** and **about 96 percent got a seniority level** from deterministic rules alone. No key,
no network, milliseconds.

Two design decisions make that work:

**A C-suite title carries a rank word, not a function word.** "Chief Operating Officer" contains
nothing the ordinary keyword table looks for, so C-suite titles get their own map that runs first.

**Individual contributor is a classification, not a failure.** Most titles with no seniority keyword
belong to people who manage nobody. When a department resolves and no seniority keyword fires, the
answer is `individual_contributor`, not null.

#### 🔌 Bring your own model key for the rest

The titles the rules cannot place are genuinely unusual: domain-specific roles like "Field
Application Scientist", non-corporate roles, and titles that are not in English.

Turn on the LLM fallback and point it at your own key, or leave those titles null. They come back as
`unclassified_no_llm`, **never as a guess**.

- The key is a **secret environment variable** on your copy of this actor, named `LLM_API_KEY`. It is
  not an input field.
- You pay your own provider directly. We never see the key or the traffic.
- **Only the job title is ever sent to the model. The person's name is not.**
- A missing key is not a failure. The actor classifies deterministically and reports
  `llm_status: "no_key"`.
- Answers are constrained to the two published lists. A reply outside them is discarded rather than
  passed through as a department.
- Answers are cached against the title for 90 days, so an unusual title costs one model call per 90
  days and returns the same answer every time.

### 📋 What data can Contact Classifier extract?

**21 fields** per contact.

| Field | What it holds |
|---|---|
| `department` | `sales`, `marketing`, `engineering`, `product`, `finance`, `legal`, `people`, `customer`, `operations`, `it`, `executive`, `general`, or `null` |
| `seniority` | `founder`, `c_level`, `svp`, `vp`, `head`, `director`, `senior_manager`, `manager`, `lead`, `senior`, `individual_contributor`, `junior`, or `null` |
| `seniority_rank` | 1 for founder down to 12 for junior |
| `is_decision_maker` | `true` for ranks 1 to 6 |
| `classification_method` | `deterministic`, `deterministic_ic_default`, `llm`, `unclassified_no_llm` or `unclassified` |
| `classification_rule` | The named rule that fired |
| `llm_used`, `llm_status`, `llm_provider` | Whether the fallback ran, and what happened |
| `position_status` | `verified_current`, `not_found`, `unverifiable` or `not_attempted` |
| `position_evidence_url`, `position_evidence_method` | The page the name was found on |
| `peers_found_on_page` | How many other people were visible on that page |
| `pages_attempted`, `pages_reached`, `fetch_status`, `fetch_error` | Verification coverage |
| `job_title`, `full_name`, `company_domain`, `run_date` | Echoed inputs and bookkeeping |

> ⚠️ **There are three answers to "we did not find them", not one.** `not_found` means the site
> published a people page listing **at least three other people** and this person was not on it.
> `unverifiable` means no page could be read, or the page published nobody, so **we could not check**.
> A blocked or empty site never reads as a departure. That is what the three-peer threshold is for,
> and `peers_found_on_page` ships on every row so you can audit the call yourself.

### 🛠️ How to classify a contact's department and seniority

1. Open the **Input** tab and put a title in `job_title`. It is the only required field.
2. Click **Start**. That is the whole deterministic path.
3. Filter on `is_decision_maker`, or on `seniority_rank` if you want your own cutoff.
4. To also verify the person, add `full_name` and `company_domain` and set `verify_position`.

#### 🧪 Using it in Clay

Add an Apify enrichment column and map your title column to `job_title`. Every field is a top-level
key.

This is the column that replaces two AI columns. Because the deterministic path needs no key and no
network, it is fast enough to run across a whole table rather than on a shortlist.

#### 🔍 Turning on position verification

Set `verify_position` and supply a full name and a company domain. The actor reads up to eight team
and leadership pages on that domain and looks for the person. It adds about 3 seconds and 9 requests.

**It only works for people a company publishes**, which means executives and leadership, almost never
individual contributors. Only about 6 companies in 10 publish a readable people page at all.

**Search alone is not enough to verify anyone.** In the build sample, a domain-scoped search returned
zero results naming two executives who were plainly listed on their own company's leadership page.
The verdict always comes from the page, never from a search.

### 💵 How much does it cost to classify a contact?

One event per contact classified, including unclassified ones.

| Plan | Per contact | Per 1,000 contacts |
|---|---|---|
| Free | $0.003 | $3.00 |
| Bronze | $0.00285 | $2.85 |
| Silver | $0.0027 | $2.70 |
| Gold | $0.00255 | $2.55 |

There is also an Actor start event at $0.00005, charged once per run per GB of memory. This actor
runs at 256 MB, so that is one start event per run.

> 💳 **Your model spend is not part of this price and never passes through us.** That is the point of
> bringing your own key: there is no variable model cost sitting inside a fixed per-event price. Every
> contact is billed including unclassified ones, because the rules ran either way, and position
> verification does not add a charge.

### ⌨️ Input

Everything is on the **Input** tab.

| Field | Type | Default | What it does |
|---|---|---|---|
| `job_title` | string | required | The only required field. |
| `full_name` | string | none | Only used for verification. |
| `company_domain` | string | none | Only used for verification. |
| `verify_position` | boolean | `false` | Adds about 3 seconds and 9 requests. |
| `use_llm_fallback` | boolean | `false` | Needs `LLM_API_KEY` set as a secret env var. |
| `llm_provider` | string | `openai` | `openai`, `anthropic` or `google`. |
| `llm_model` | string | `gpt-4o-mini` | Passed straight through. |
| `skipCache` | string | `"false"` | `"true"` classifies from scratch. |

### 📤 Output

One flat row per contact, exportable as **JSON, CSV, Excel, HTML or XML**.

```json
{
  "job_title": "Chief Operating Officer",
  "full_name": "Jane Doe",
  "company_domain": "example.com",
  "department": "executive",
  "seniority": "c_level",
  "seniority_rank": 2,
  "is_decision_maker": true,
  "classification_method": "deterministic",
  "classification_rule": "c_suite_map:chief_operating_officer",
  "llm_used": false,
  "llm_status": "not_needed",
  "position_status": "verified_current",
  "position_evidence_url": "https://example.com/about/leadership",
  "peers_found_on_page": 7,
  "pages_attempted": 8,
  "pages_reached": 3,
  "fetch_status": "ok",
  "fetch_error": null,
  "run_date": "2026-08-10T10:19:37.363Z"
}
```

### 💡 Tips

- Filter on `seniority_rank` rather than `is_decision_maker` when you disagree with where the cutoff
  sits. That is why the integer ships.
- Run the deterministic path across your whole table first. Only the leftovers are worth an LLM call.
- Read `classification_rule` when a title surprises you. It names the rule that fired, so you can
  rebuild the reading instead of trusting it.
- Treat `unverifiable` as "unknown", never as "gone". `peers_found_on_page` tells you whether the page
  was worth anything.

### ⚠️ Known limits

**Seniority is a ladder, not a judgment.** `individual_contributor` means the title carries no
management keyword. Some of the most senior people in a company have no management keyword in their
title.

**`is_decision_maker` cuts at director.** That is one constant and it is arbitrary. If you disagree
with where it sits, filter on `seniority_rank` instead.

**Titles are matched in English.** A non-English title will usually fall through to the LLM path or
come back null.

**Null is never a guess.** An unplaceable title returns null. `general` is a real answer for titles
like Analyst and Consultant, not a dumping ground.

**Verification only works for people a company publishes,** which is executives and leadership, almost
never individual contributors, and only about 6 companies in 10 publish a readable people page at all.

**Every contact is billed, including unclassified ones.** The rules ran either way.

**No proxy.** Verification fetches company pages directly. A site that blocks the actor is reported as
`unverifiable`, not routed around. `robots.txt` is read and honored before any page is requested.

### ❓ FAQ

##### Do I need an API key?

No. Classification is deterministic and needs no key, no network and no configuration. The key is only
for the optional fallback on unusual titles.

##### What happens if I turn on the LLM fallback but the key is missing?

Nothing breaks. The actor classifies deterministically and reports `llm_status: "no_key"`.

##### Is the person's name sent to the model?

No, never. Only the job title is sent, and only when the fallback runs.

##### Does `not_found` mean the person left the company?

No. It means the employer published a people page listing at least three other people and this person
was not on it. If no page could be read, you get `unverifiable` instead, which means the check did not
happen.

##### Why did an obviously senior person classify as `individual_contributor`?

Because their title carries no management keyword. That is the honest reading of the string, and
`classification_rule` will tell you which rule produced it.

### 🧩 Want other GTM data?

Mamba Labs builds custom actors for B2B go-to-market teams. The public versions
of that work live here on the Store, so our users get the same tooling we build
under contract.

| | |
|---|---|
| 🧑‍💼 [GTM Hiring Signal Scraper](https://apify.com/mambalabs/gtm-hiring-signal-scraper) | 🧱 [Tech Stack Detector](https://apify.com/mambalabs/gtm-tech-stack-signal-scraper) |
| 📡 [B2B Buying Signals Aggregator](https://apify.com/mambalabs/b2b-buying-signals-hiring-tech-stack-intent-for-clay) | 🔑 [Job Board Keyword Scanner](https://apify.com/mambalabs/job-board-keyword-signal-scanner) |
| 🔗 [Domain to LinkedIn URL Resolver](https://apify.com/mambalabs/domain-to-linkedin-url-resolver) | 🎯 [ICP Fit Scorer](https://apify.com/mambalabs/icp-account-lead-scoring-fit-scorer-0-100-for-clay) |
| 📋 [Job Posting Monitor](https://apify.com/mambalabs/gtm-job-discovery) | 📬 [Domain Deliverability Checker](https://apify.com/mambalabs/domain-deliverability-checker) |
| 🏢 [Company Firmographic Enricher](https://apify.com/mambalabs/company-firmographic-enricher) | 🌐 [Company Social Presence Mapper](https://apify.com/mambalabs/company-social-presence-mapper) |
| 🪪 [Company Identity Resolver](https://apify.com/mambalabs/company-identity-resolver) | 💰 [Funding and Press Signal Scanner](https://apify.com/mambalabs/funding-press-signal-scanner) |
| 🔄 [Company Change-Event Feed](https://apify.com/mambalabs/company-change-event-feed) | 👤 [People Finder and Email Verifier](https://apify.com/mambalabs/people-finder) |
| 🚀 [Prospect Engine](https://apify.com/mambalabs/b2b-prospect-engine) | 🤖 [AI Tooling Detector](https://apify.com/mambalabs/ai-tooling-detector) |
| 📮 [Outbound Stack Detector](https://apify.com/mambalabs/outbound-infrastructure-fingerprint) | 📝 [Publishing Frequency Tracker](https://apify.com/mambalabs/blog-publishing-frequency) |
| ✉️ [Work Email Waterfall Finder](https://apify.com/mambalabs/email-waterfall-orchestrator) | ⏩ [Sequencer Lead Push](https://apify.com/mambalabs/clay-to-instantly-smartlead-push) |
| 🏅 [Workplace Program Detector](https://apify.com/mambalabs/workplace-program-detector) | 👥 [Team Page People Extractor](https://apify.com/mambalabs/team-page-people-extractor) |
| 🧭 [Company Discovery List Builder](https://apify.com/mambalabs/company-discovery-list-builder) | |

> Every actor in the suite takes a domain or a company and returns one flat row,
> so they stack in the same Clay table without reshaping anything.

> 🛠️ **Need something custom built for you or your team?** Tell us what you are
> trying to find and we will build it. [Talk to Mamba Labs](https://mambabuilt.com/contact).

### 🆘 Support

Found a title the rules place wrongly? Open an issue on the **Issues** tab with the title and the
`classification_rule` from the row, and we will look at it.

> ℹ️ **Privacy, sourcing and legal.** The classification half touches no personal data at all if you
> send only a title. Verification does process personal data, because a name plus an employer plus a
> role is personal data. The name and title come from you; this actor does not discover people. It
> returns no email, no phone, no photograph and no biography, nothing beyond the two labels read off
> the title and a status. Every verified row carries `position_evidence_url`, so any statement about a
> person traces to a page their employer published, read directly with `robots.txt` honored. The
> verification cache holds the name and domain for 7 days only, and the person's name is never sent to
> any language model. You are the data controller for anything you send here.

Built by [Mamba Labs](https://apify.com/mambalabs).

# Actor input Schema

## `job_title` (type: `string`):

The contact's job title, exactly as you hold it. Classified by deterministic rules with no API key needed.

## `full_name` (type: `string`):

Only needed for position verification. Classification works without it. This name is never sent to any language model.

## `company_domain` (type: `string`):

Only needed for position verification. The company's website domain, with or without https.

## `verify_position` (type: `boolean`):

Check whether the person is still listed on their employer's own website. Off by default. Adds roughly 3 seconds and 9 requests per contact, and needs both the name and the domain.

## `use_llm_fallback` (type: `boolean`):

Off by default. When on, titles the rules cannot place are sent to your own model using the LLM\_API\_KEY secret environment variable you set on your copy of this Actor. Only the title is sent, never the person's name. With no key set the Actor still returns a row, it just leaves those titles null.

## `llm_provider` (type: `string`):

Which provider your LLM\_API\_KEY belongs to. Only read when the LLM fallback is on.

## `llm_model` (type: `string`):

Model id passed straight through to the provider. Only read when the LLM fallback is on.

## `skipCache` (type: `string`):

Set to true to ignore cached results and classify from scratch.

## Actor input object example

```json
{
  "job_title": "Chief Operating Officer",
  "full_name": "Yamini Rangan",
  "company_domain": "hubspot.com",
  "verify_position": false,
  "use_llm_fallback": false,
  "llm_provider": "openai",
  "llm_model": "gpt-4o-mini",
  "skipCache": "false"
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "job_title": "Vice President, Proposal Operations",
    "verify_position": false,
    "use_llm_fallback": false,
    "llm_provider": "openai",
    "llm_model": "gpt-4o-mini",
    "skipCache": "false"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mambalabs/contact-classifier").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "job_title": "Vice President, Proposal Operations",
    "verify_position": False,
    "use_llm_fallback": False,
    "llm_provider": "openai",
    "llm_model": "gpt-4o-mini",
    "skipCache": "false",
}

# Run the Actor and wait for it to finish
run = client.actor("mambalabs/contact-classifier").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "job_title": "Vice President, Proposal Operations",
  "verify_position": false,
  "use_llm_fallback": false,
  "llm_provider": "openai",
  "llm_model": "gpt-4o-mini",
  "skipCache": "false"
}' |
apify call mambalabs/contact-classifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mambalabs/contact-classifier"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0lGSeYJmniXhGANnO/builds/ph6iDjS0nJF6uKvcV/openapi.json
