# Work Email Waterfall Finder Verifier BYOK Clay Ready (`mambalabs/email-waterfall-orchestrator`) Actor

Finds work emails by chaining your own provider keys, verifies each result, and returns real per-provider spend instead of hiding it behind a credit abstraction. Bring your own Findymail, Prospeo, Icypeas, Reoon and BounceBan keys.

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

## Pricing

from $20.00 / 1,000 email founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Email Waterfall and Verification Orchestrator

Finds a work email for each contact by chaining several finder providers, verifies every result, and returns **real per-provider spend** instead of hiding it behind a credit abstraction.

**Bring your own keys.** You supply your own Findymail, Prospeo, Icypeas, Reoon and BounceBan keys. We supply none, we never see your credits, and you pay your providers directly at your own rates.

### Why this exists

Clay's Work Email waterfall works, and it hides what each provider cost you behind Clay credits. That is the top recorded complaint about its pricing. This returns the per-provider detail: which entry answered, what it consumed, how long it took, and what your balance actually moved by.

Two things Clay structurally does not do, and this does:

- **Real per-provider attribution.** Units per provider per endpoint, every attempt logged, plus your observed balance movement read from each provider's own account endpoint before and after the run.
- **A per-provider circuit breaker.** A provider that runs out of credit or fails auth is disabled once for the rest of the run instead of being retried on every contact.

### What you get back

One row per input contact, always, including the ones where nothing was found, plus a run summary row.

| Field | What it is |
|---|---|
| `email` | The work email found, or null |
| `email_status` | `valid`, `catch_all`, `catch_all_resolved`, `unknown`, `unverified`, `invalid`, `not_found` and a few run states |
| `found_by` | The registry entry that answered, for example `findymail.search_business_profile#linkedin_url`. The identifier binding is part of the id on purpose |
| `chain_position` | Which position in your order returned it |
| `verification_source` | Which verifier's answer stands |
| `verification_degraded` | True when the primary verifier failed and the chain fell back |
| `providers_attempted` | Every entry tried, with binding, outcome, latency and units consumed |
| `provider_units_used` | Credits this row consumed from your accounts |

`invalid` appears **only** when a verification provider actually looked and said the address will not deliver. A verifier that failed, timed out or was out of credit never produces `invalid`, because a verification failure is not evidence that an address is dead.

### Give it a LinkedIn URL if you have one

Measured on 300 contacts, one key, one hour:

| Input | Hit rate |
|---|---|
| LinkedIn URL | **13.7%** |
| Full name plus company domain | **9.3%** |

14 of the hits were unique to the LinkedIn binding and 1 to name plus domain. Same provider, same hour, only the input field changed. Supply every identifier you have; the chain uses the best one each provider accepts and never wastes a call on a provider whose required field you did not supply.

Hit rates are cohort dependent. Those numbers are from one B2B cohort and yours will differ.

### Input

```json
{
  "contacts": [
    {
      "id": "your-own-id",
      "full_name": "Satya Nadella",
      "company_domain": "microsoft.com",
      "linkedin_url": "https://www.linkedin.com/in/satyanadella/"
    }
  ],
  "providerKeys": { "findymail": "...", "prospeo": "...", "bounceban": "..." },
  "maxProviderUnits": 500
}
```

A single contact is an array of one. `contacts` and `providerKeys` are also accepted as JSON strings, because Clay sends every input as a string.

**Set `maxProviderUnits`.** It is a hard ceiling on the provider credits a run may consume from your accounts. This actor spends your money, not ours.

Supply `providerRates` with your own per-unit costs and the summary reports dollars at your rates. Leave it out and it reports units only. With your own keys we do not know your plan tier or your negotiated rate, and we will not assume one.

### The default chain

1. `findymail.search_business_profile#linkedin_url`
2. `findymail.search_name#name_domain`
3. `prospeo.enrich_person#any`

Then verification, after each finder that returns an address rather than once at the end, because providers do not agree on what "found" means.

**Icypeas is included but off by default.** In 300 independent contacts it returned zero hits that Findymail had not already found, and its result-read endpoint is limited to 30 calls per minute, which puts a floor of roughly 33 minutes under a 1,000-row batch. Add `icypeas.email_search#name_domain` to `providerOrder` if you want it; its bulk path is implemented and is much faster for large batches.

**No regional presets ship.** We have no regional accuracy data, and shipping presets without it would be inventing them.

### Consensus threshold

Default 0, matching Clay's default. Worth knowing: Clay's "threshold for duplicate results" is **not** a consensus vote. Clay's own documentation describes it as an early-exit cost control that stops the waterfall when the same address keeps failing validation across providers. They are different features and should not be confused.

### No provider keys?

The actor still runs, pushes a summary row explaining which capabilities were unavailable, charges nothing and exits cleanly. It never treats a missing key as a crash.

Every key you do supply is validated for free before any paid call. A provider whose key is missing, invalid or out of credit is dropped from the chain and reported, rather than failing per contact.

### Cost

Compute is negligible for this shape because it waits on APIs rather than crawling: measured at well under a tenth of one percent of a typical per-result price in batch. Your real cost is your own provider credits, which is exactly what the summary reports.

### Personal email

Out of scope. This finds work emails only.

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

# Actor input Schema

## `contacts` (type: `array`):

One object per person. Each may carry full\_name, first\_name, last\_name, company\_domain, company\_name, linkedin\_url and email. More identifiers means a better chance: keyed on a LinkedIn URL the top provider measured 13.7 percent against 9.3 percent on name plus domain, on the same 300 contacts in the same hour. A single contact is an array of one. Also accepted as a JSON string, because Clay sends every input as a string.

## `providerKeys` (type: `object`):

Your own keys, by provider name: findymail, prospeo, icypeas, reoon, bounceban. Every key is validated for free before any paid call, and a provider with no usable key is dropped from the chain and reported rather than failing the run. With no keys at all the actor completes, explains what was unavailable and charges nothing.

## `providerOrder` (type: `array`):

Registry entry ids in the order you want them called. Leave empty for the default: findymail.search\_business\_profile#linkedin\_url, findymail.search\_name#name\_domain, prospeo.enrich\_person#any. Icypeas is registered but off by default: it returned zero marginal hits in 300 independent contacts and its result-read endpoint is 30 per minute, which puts a 33-minute floor under a 1,000-row batch. Add icypeas.email\_search#name\_domain here if you want it.

## `verificationOrder` (type: `array`):

Verifier entry ids in order. Default is reoon.verify\_email#email then bounceban.verify\_email#email. Verification runs after EACH finder that returns an address, not once at the end, because providers do not agree on what found means.

## `maxContacts` (type: `integer`):

Hard ceiling on how many contacts are processed. Leave empty for no ceiling.

## `maxProviderUnits` (type: `integer`):

Run-level cap on the provider credits this run may consume from YOUR accounts. When it is reached the remaining contacts are skipped with a reason rather than spent on. Strongly recommended: this actor spends your money, not ours.

## `providerRates` (type: `object`):

Optional map of provider or provider.endpoint to YOUR cost per unit, for example {"findymail.search\_name": 0.0198}. Supply it and the run reports dollars at YOUR rate. Leave it empty and the run reports units only, because with your own keys we do not know your plan tier or negotiated rate and will not assume one.

## `concurrencyHint` (type: `integer`):

Advisory. Real pacing comes from each endpoint's own documented rate limits, which are multi-window and per endpoint, so this cannot exceed them.

## Actor input object example

```json
{
  "contacts": [],
  "providerOrder": [],
  "verificationOrder": [],
  "maxContacts": 1000,
  "maxProviderUnits": 500,
  "providerRates": {},
  "concurrencyHint": 4
}
```

# 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 = {
    "contacts": [],
    "providerOrder": [],
    "verificationOrder": [],
    "maxContacts": 1000,
    "maxProviderUnits": 500,
    "providerRates": {},
    "concurrencyHint": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("mambalabs/email-waterfall-orchestrator").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 = {
    "contacts": [],
    "providerOrder": [],
    "verificationOrder": [],
    "maxContacts": 1000,
    "maxProviderUnits": 500,
    "providerRates": {},
    "concurrencyHint": 4,
}

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

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

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

```

## CLI example

```bash
echo '{
  "contacts": [],
  "providerOrder": [],
  "verificationOrder": [],
  "maxContacts": 1000,
  "maxProviderUnits": 500,
  "providerRates": {},
  "concurrencyHint": 4
}' |
apify call mambalabs/email-waterfall-orchestrator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=mambalabs/email-waterfall-orchestrator",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OT6xqTFThC0Rjf7Zj/builds/RcSNi5IZP8J0Onnog/openapi.json
