All docs

Enrichment

Lookup contacts and companies, run waterfall enrichment, and verify emails

The enrichment endpoints cover three capabilities:

  • Lookup — Find a single person or company from the open data index when you already know their email, LinkedIn URL, or domain. Returns full profile data instantly.
  • Enrich — Fill missing fields (emails, phone numbers) on contacts you already have in your workspace by running them through multiple data providers in sequence (waterfall enrichment).
  • Verify — Check if an email address is deliverable before sending.

Two billing buckets

Enrichment endpoints split into two buckets that bill differently. The split matters because graph8 owns the cost on one side and pays a third-party provider on the other.

Index Lookups - free on both plans

These endpoints hit graph8’s owned indexes (OpenSearch + ClickHouse). Single-record, instant, no waterfall.

EndpointPAYGPlatform
POST /enrichment/lookup/personFree (within 50 rps)Free (within 50 rps)
POST /enrichment/lookup/companyFree (within 50 rps)Free (within 50 rps)
POST /enrichment/verify-email (internal validator)Free (within 50 rps)Free (within 50 rps)

Waterfall Enrichment - always credits

These endpoints chain through external providers (Prospeo, Dropcontact, Cognism, and others). graph8 pays the provider per record - so credits apply on both PAYG and Platform.

EndpointPAYGPlatform
POST /enrichment/enrich (waterfall)Variable per recordVariable per record
External email verifiers (Kickbox, ZeroBounce)Per-verificationPer-verification

See Pricing for the full plan comparison.


Person Lookup

POST /enrichment/lookup/person

Instant person lookup. Provide at least one of: email, linkedin_url, or first_name + last_name + company_domain.

Pricing: Free on both PAYG and Platform within the 50 rps cap (graph8-owned index data).

Request Body

FieldTypeRequiredDescription
emailstringNo*Work email address
linkedin_urlstringNo*LinkedIn profile URL
first_namestringNo*First name (requires last_name + company_domain)
last_namestringNo*Last name (requires first_name + company_domain)
company_domainstringNo*Company domain (requires first_name + last_name)

*At least one lookup strategy is required.

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/lookup/person" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'

Python

response = requests.post(
    f"{BASE_URL}/enrichment/lookup/person",
    headers=HEADERS,
    json={"email": "[email protected]"}
)

Response

{
  "data": {
    "found": true,
    "confidence": 0.95,
    "data": {
      "first_name": "Jane",
      "last_name": "Doe",
      "job_title": "VP of Sales",
      "company": "Acme Inc",
      "linkedin_url": "https://linkedin.com/in/janedoe",
      "work_email": "[email protected]"
    }
  }
}

Company Lookup

POST /enrichment/lookup/company

Instant company lookup by domain or name.

Pricing: Free on both PAYG and Platform within the 50 rps cap (graph8-owned index data).

Request Body

FieldTypeRequiredDescription
domainstringNo*Company domain
namestringNo*Company name

*At least one of domain or name is required.

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/lookup/company" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "acme.com"}'

Python

response = requests.post(
    f"{BASE_URL}/enrichment/lookup/company",
    headers=HEADERS,
    json={"domain": "acme.com"}
)

Response

{
  "data": {
    "found": true,
    "confidence": 0.98,
    "data": {
      "name": "Acme Inc",
      "domain": "acme.com",
      "industry": "Technology",
      "employee_count": 500,
      "annual_revenue": "$50M-$100M"
    }
  }
}

Start Enrichment Job

POST /enrichment/enrich

Start an async waterfall enrichment job. The job runs across multiple data providers and returns a job_id you can poll for results.

Returns 202 Accepted.

Pricing: Credits apply on both PAYG and Platform - waterfall enrichment hits third-party providers (Prospeo, Dropcontact, Cognism, etc.) with per-record cost. Cost varies by which providers are configured in fields_config.

Request Body

FieldTypeRequiredDescription
contact_idsinteger[]YesContact IDs to enrich
list_idintegerYesList ID the contacts belong to
fields_configobjectNoFields to enrich and provider order (see below)

fields_config example:

{
  "work_email": ["prospeo", "dropcontact"],
  "direct_phone": ["cognism", "lusha"]
}

If omitted, defaults to {"work_email": ["prospeo", "dropcontact"]}.

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/enrich" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_ids": [101, 102, 103],
    "list_id": 5,
    "fields_config": {"work_email": ["prospeo", "dropcontact"]}
  }'

Python

response = requests.post(
    f"{BASE_URL}/enrichment/enrich",
    headers=HEADERS,
    json={
        "contact_ids": [101, 102, 103],
        "list_id": 5,
        "fields_config": {"work_email": ["prospeo", "dropcontact"]}
    }
)
job_id = response.json()["data"]["job_id"]

Response

{
  "data": {
    "job_id": "abc-123-def",
    "status": "queued",
    "total": null,
    "completed": null,
    "results": null
  }
}

Poll Enrichment Job

GET /enrichment/jobs/{job_id}

Check the status of an enrichment job. Works for both POST /enrichment/enrich and POST /enrichment/waterfall/enrich jobs.

Path Parameters

ParameterTypeDescription
job_idstringJob UUID returned by the enrich endpoint

Query Parameters

ParameterTypeDefaultDescription
include_provider_errorsbooleanfalseWhen true, adds a provider_errors array with per-record, per-provider error details (one extra DB read)

Status Values

StatusDescription
queuedJob is waiting to start
runningEnrichment in progress
completedPipeline ran to completion (check successful_enrichments for actual results)
failedJob failed
cancelledJob was cancelled

Example

cURL

curl "https://be.graph8.com/api/v1/enrichment/jobs/abc-123-def" \
  -H "Authorization: Bearer $API_KEY"

# With provider error details
curl "https://be.graph8.com/api/v1/enrichment/jobs/abc-123-def?include_provider_errors=true" \
  -H "Authorization: Bearer $API_KEY"

Python

response = requests.get(
    f"{BASE_URL}/enrichment/jobs/{job_id}",
    headers=HEADERS,
    params={"include_provider_errors": True}
)
data = response.json()["data"]

Response

{
  "data": {
    "job_id": "abc-123-def",
    "status": "completed",
    "total": 100,
    "completed": 100,
    "results": null,
    "successful_enrichments": 0,
    "failed_enrichments": 100,
    "total_credits_used": 0,
    "warnings": null,
    "provider_errors": null
  }
}

successful_enrichments, failed_enrichments, and total_credits_used are populated only for terminal jobs (completed, failed, cancelled). They are null while a job is queued or running. When include_provider_errors=true, provider_errors contains entries like {"record_id": 101, "provider": "hunter", "error": "hunter API key is not configured"}.


Verify Email

POST /enrichment/verify-email

Verify a single email address and check its deliverability status.

Pricing: Free on both PAYG and Platform within the 50 rps cap when graph8’s internal validator can answer. Calls that fall through to third-party verifiers (Kickbox, ZeroBounce) consume 1 credit per verify on both plans.

Request Body

FieldTypeRequiredDescription
emailstringYesEmail address to verify

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/verify-email" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'

Python

response = requests.post(
    f"{BASE_URL}/enrichment/verify-email",
    headers=HEADERS,
    json={"email": "[email protected]"}
)

Response

{
  "data": {
    "email": "[email protected]",
    "status": "valid",
    "sub_status": null,
    "is_valid": true
  }
}
StatusDescription
validDeliverable email address
invalidUndeliverable
catch-allDomain accepts all addresses
unknownCould not determine

List Waterfall Configs

GET /enrichment/waterfall/configs

Returns all waterfall enrichment pipelines configured for a list. Each config includes per-step credential information so you can tell which providers are ready to run.

Query Parameters

ParameterTypeDefaultDescription
list_idintegerRequired. List ID to fetch configs for

Example

cURL

curl "https://be.graph8.com/api/v1/enrichment/waterfall/configs?list_id=12345" \
  -H "Authorization: Bearer $API_KEY"

Response

{
  "data": {
    "configs": [
      {
        "column_id": "waterfall_xxx",
        "name": "Email lookup",
        "field_to_enrich": "work_email",
        "data_type": "text",
        "is_global": false,
        "is_active": true,
        "list_id": 12345,
        "step_count": 1,
        "providers": ["graph8"],
        "providers_detail": [
          {
            "provider": "graph8",
            "is_system_provider": true,
            "credential_source": "system",
            "credential_ready": true
          }
        ],
        "missing_credentials": []
      }
    ],
    "list_id": 12345,
    "count": 1
  }
}

providers_detail gives the per-step credential breakdown (system vs BYOK and readiness). missing_credentials lists providers whose BYOK step has no active org key — those steps will be skipped at run time.


Create Waterfall Config

POST /enrichment/waterfall/configs

Creates (or reuses) a waterfall enrichment pipeline. Idempotent by (list_id, name) — calling it twice with the same name returns the existing config with "created": false.

Returns 201 Created.

Three call styles are available, in increasing order of control:

  1. Bare — pass only list_id. Server builds a single-step graph8 lookup (0 credits). Pre-2026-06 contract.
  2. By providers — pass providers=["hunter","prospeo"]. Server maps each to its default email-finder action in that order. Use this for “try these providers, in this order” pipelines.
  3. By steps — pass steps=[...] with full step shapes. Server persists them verbatim. Use this for provider-specific config (e.g. Apollo reveal_phone_numbers=true).

providers and steps are mutually exclusive — pass one or neither. email_verification may be combined with any call style.

Request Body

FieldTypeRequiredDescription
list_idintegerYesList ID to create the config for
namestringNoPipeline name (default assigned if omitted)
field_to_enrichstringNoField to fill in (default work_email)
skip_existing_valuesbooleanNoSkip records that already have a value (default true)
providersstring[]NoOrdered list of provider names — server builds default email-finder steps. Mutually exclusive with steps.
stepsobject[]NoFull caller-supplied step objects (see below). Mutually exclusive with providers.
email_verificationobjectNoPost-enrichment email validation config (see below)
credential_modestringNoauto | system | byok — controls how providers[] resolves credentials (default auto). Has no effect on steps[].

steps[] object fields:

FieldTypeDescription
providerstringProvider name (e.g. hunter)
actionstringProvider action (e.g. find_email)
input_mappingobjectMaps provider input keys to contact field tokens
output_fieldstringContact field to write the result to (e.g. contact.work_email)
orderintegerStep execution order (0-indexed)
is_system_providerbooleantrue to use graph8’s system key (charges credits); false to use the org’s BYOK key
configobjectProvider-specific flags (e.g. {"reveal_phone_numbers": true} for Apollo)

email_verification object fields:

FieldTypeDescription
enabledbooleanWhether to run email verification after enrichment
providerstringVerifier to use: zerobounce, hunter, icypeas, or leadmagic
use_system_credentialsbooleanUse graph8 system key (charges credits)
accept_catchallbooleanTreat catch-all addresses as valid
valid_statusesstring[]Statuses to accept (e.g. ["valid"])

credential_mode values:

ValueBehavior
auto(Default, matches the UI) Use the org’s BYOK key when configured (free); otherwise use graph8’s system key for funded providers (charges credits). Unfunded providers (lusha, rocketreach) stay BYOK.
systemForce graph8 system keys for funded providers (always charges credits).
byokForce the org’s own keys for every non-graph8 provider. Steps silently skip when no key exists.

graph8 is always system-funded regardless of mode. graph8 holds system keys for: graph8, apollo, hunter, builtwith, dropcontact, prospeo, semrush, icypeas, zerobounce, leadmagic, emaillistverify.

Allowed providers[] values: graph8, hunter, prospeo, dropcontact, icypeas, leadmagic, emaillistverify, apollo, lusha, rocketreach. Unknown providers return 400 unsupported_provider. For other providers (e.g. zerobounce as a finder) use steps[].

Example

cURL

# By providers shortcut (recommended)
curl -X POST "https://be.graph8.com/api/v1/enrichment/waterfall/configs" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 12345,
    "name": "Email Pipeline",
    "providers": ["hunter", "prospeo"],
    "credential_mode": "auto",
    "email_verification": {
      "enabled": true,
      "provider": "zerobounce",
      "use_system_credentials": true,
      "accept_catchall": false,
      "valid_statuses": ["valid"]
    }
  }'

# By steps (full control)
curl -X POST "https://be.graph8.com/api/v1/enrichment/waterfall/configs" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": 12345,
    "name": "Apollo with phones",
    "steps": [{
      "provider": "apollo",
      "action": "find_email",
      "input_mapping": {"first_name": "CONTACT_FIRST_NAME", "last_name": "CONTACT_LAST_NAME"},
      "output_field": "contact.work_email",
      "order": 0,
      "is_system_provider": true,
      "config": {"reveal_phone_numbers": true}
    }]
  }'

Response

{
  "data": {
    "column_id": "waterfall_xxx",
    "config_id": "uuid",
    "name": "Email Pipeline",
    "list_id": 12345,
    "created": true,
    "providers": ["hunter", "prospeo"],
    "email_verification": {
      "enabled": true,
      "provider": "zerobounce",
      "use_system_credentials": true,
      "accept_catchall": false,
      "valid_statuses": ["valid"]
    },
    "providers_detail": [
      {
        "provider": "hunter",
        "is_system_provider": true,
        "credential_source": "system",
        "credential_ready": true
      },
      {
        "provider": "prospeo",
        "is_system_provider": false,
        "credential_source": "byok",
        "credential_ready": false
      }
    ],
    "missing_credentials": ["prospeo"]
  }
}

missing_credentials lists BYOK steps with no active org key — those steps will be skipped at run time. Call GET /enrichment/providers to see which providers need a BYOK key for your org.

Errors:

  • 400providers and steps are mutually exclusive (pass one, not both)
  • 400 unsupported_providerproviders[] contained a name without a default email-finder mapping

Run Waterfall Enrichment

POST /enrichment/waterfall/enrich

Runs an existing waterfall pipeline against a set of records. Returns 202 Accepted — poll GET /enrichment/jobs/{job_id} for the outcome.

When a pipeline step is BYOK with no usable org key (or an unfunded provider like lusha or rocketreach), the 202 response carries a non-blocking warnings array naming those providers. The job still queues, but those steps will be skipped.

Request Body

FieldTypeRequiredDescription
column_idstringYesWaterfall config ID from GET /enrichment/waterfall/configs
list_idintegerYesList the records belong to
record_idsinteger[]YesNon-empty list of contact IDs to enrich
skip_existing_valuesbooleanNoOverride the pipeline’s skip_existing_values setting for this run

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/waterfall/enrich" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "column_id": "waterfall_xxx",
    "list_id": 12345,
    "record_ids": [101, 102, 103],
    "skip_existing_values": true
  }'

Response

{
  "data": {
    "job_id": "abc-123-def",
    "status": "queued",
    "total": null,
    "completed": null,
    "results": null,
    "warnings": ["prospeo: no active org key configured — step will be skipped"]
  }
}

warnings is a non-blocking array present when one or more pipeline steps are BYOK with no usable org key (or use an unfunded provider like lusha or rocketreach). The job still queues and runs; those steps are skipped. warnings is null when all configured steps have valid credentials.

Poll GET /enrichment/jobs/{job_id} (optionally with include_provider_errors=true) to check successful_enrichments when the job reaches completed.

Errors:

  • 422 waterfall_config_missing — no active waterfall pipeline exists for the supplied column_id. Fix by calling POST /enrichment/waterfall/configs to create or restore the config.

List Enrichment Providers

GET /enrichment/providers

Lists every enrichment provider available to the calling org, including its credential mode and per-action credit cost. This is the programmatic equivalent of the UI’s provider picker — call it before building a pipeline to see which providers run on graph8 credits by default vs need your own API key.

Example

cURL

curl "https://be.graph8.com/api/v1/enrichment/providers" \
  -H "Authorization: Bearer $API_KEY"

Response

{
  "data": {
    "providers": [
      {
        "provider": "hunter",
        "system_funded": true,
        "byok_configured": false,
        "default_credential_source": "system",
        "credits_per_row": {
          "find_email": 2.0,
          "verify_email": 2.0,
          "domain_search": 2.0
        },
        "actions": ["domain_search", "find_email", "verify_email"]
      },
      {
        "provider": "lusha",
        "system_funded": false,
        "byok_configured": false,
        "default_credential_source": "byok",
        "credits_per_row": {
          "enrich_person": 2.0,
          "enrich_company": 2.0
        },
        "actions": ["enrich_company", "enrich_person"]
      }
    ],
    "count": 2
  }
}
FieldDescription
system_fundedgraph8 holds a system key for this provider — works out of the box and charges credits
byok_configuredThe org has its own API key configured for this provider
default_credential_sourceWhat credential_mode="auto" resolves to right now (system or byok)
credits_per_rowPer-action credit cost when running on graph8’s system key
actionsAvailable actions for this provider

List AI Enrichment Configs

GET /enrichment/ai/configs

Returns all AI enrichment configurations for a list.

Query Parameters

ParameterTypeDefaultDescription
list_idintegerRequired. List ID to fetch AI configs for

Example

cURL

curl "https://be.graph8.com/api/v1/enrichment/ai/configs?list_id=12345" \
  -H "Authorization: Bearer $API_KEY"

Response

{
  "data": {
    "configs": [
      {
        "group_id": "ai_enrich_xxx",
        "name": "Company research",
        "usecase": "web-research",
        "list_id": 12345,
        "is_global": false,
        "is_active": true
      }
    ],
    "list_id": 12345,
    "count": 1
  }
}

Run AI Enrichment

POST /enrichment/ai/enrich

Runs an AI enrichment config against a set of records. This endpoint is synchronous — it returns when the enrichment is complete (unlike POST /enrichment/waterfall/enrich which is async).

Request Body

FieldTypeRequiredDescription
group_idstringYesAI enrichment config ID from GET /enrichment/ai/configs
list_idintegerYesList the records belong to
record_idsinteger[]YesNon-empty list of contact IDs to enrich
max_recordsintegerNoMaximum number of records to process in this call
skip_existing_valuesbooleanNoSkip records that already have a value

Example

cURL

curl -X POST "https://be.graph8.com/api/v1/enrichment/ai/enrich" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "group_id": "ai_enrich_xxx",
    "list_id": 12345,
    "record_ids": [101, 102],
    "max_records": 50,
    "skip_existing_values": true
  }'

Response

{
  "data": {
    "job_id": "abc-456-ghi",
    "status": "completed",
    "total": 2,
    "completed": 2,
    "results": [
      {
        "processed_records": 2,
        "successful_enrichments": 2,
        "failed_enrichments": 0
      }
    ]
  }
}

Waterfall Credit Matrix

Waterfall enrichment (POST /enrichment/enrich) runs each contact / company through a sequence of 14 third-party providers. Only successful provider hits charge credits — if no provider matches, the entire run is free. Cached results (same record + field, within the 7-day cache TTL) also return for free.

Per-provider hit cost

ProviderReturnsEmailPhoneOther
ApolloEmail, phones, title, seniority, company, LinkedIn12 (direct), 2 (mobile)LinkedIn 1, title 1
HunterEmail, domain1Domain 1, verify 2
ProspeoEmail, direct phone12
DropContactEmail, phone, company12 (direct), 2 (mobile)
LushaEmail, direct + mobile phone12 (direct), 2 (mobile)
IcypeasEmail, phone, LinkedIn, title, company11Domain 0.5, LinkedIn 1, company 1
RocketReachEmail, phone, company, LinkedIn222
LeadMagicEmail, mobile, LinkedIn, company, industry, funding0.53 (mobile)LinkedIn 3, company 2, funding 2.5
ClearbitCompany domain, industry, headcount, title2
BuiltWithCompany tech stack, domain relationships2 per action
SemrushTraffic, keywords, competitors, social1 per result returned
graph8 internalMashup contacts + companies2 per lookup (free if using org-owned API key)

Email verifier add-on cost

Run after a provider returns an email; if invalid the waterfall continues to the next provider.

ProviderVerify costNotes
LeadMagic0.5Cheapest option
EmailListVerify1Dedicated verifier
ZeroBounce1 (verify) / 20 (find_email)Find_email is the expensive outlier
Icypeas1Also a data-finding provider
Hunter2Slightly higher than competitors

Default waterfall order

Email (work_email / contact_email): Apollo → Hunter → Prospeo → DropContact → Lusha → Icypeas → RocketReach → LeadMagic → (optional verifier: EmailListVerify / ZeroBounce).

Direct phone: Apollo → Prospeo → DropContact → Lusha.

Mobile phone: Apollo → DropContact → Lusha → LeadMagic.

Company domain: Clearbit → Apollo → Hunter.

Industry / size: Clearbit only.

Order is configurable per org via the in-app waterfall editor — enrichment_configs row drives the live sequence.

When credits are (and aren’t) charged

ScenarioCharged?
Provider returns a matchYes — cost of that field for that provider
Provider returns no matchNo — waterfall moves on
All providers exhausted, no matchNo charge at all
Cached result returned (same field, same record, within 7d TTL)Free
Provider call uses your own API key (configured in the org)Free
Record skipped because run conditions weren’t metFree
Email verifier validates an email as validVerifier cost absorbed; no extra charge
Email verifier rejects an emailVerifier cost charged; waterfall continues for free

Apollo phone-reveal flags

Apollo offers two add-on flags that cost extra credits beyond the base hit:

FlagExtra credits
reveal_phone_numbers=true+3 to +15 (depends on number type)
reveal_personal_emails=true+1 to +7

Both flags default to off to avoid surprise spend. Enable per call when you specifically need direct phone or personal email.

Credit hold + batch charge

For large enrichment jobs the system pre-holds a credit budget then charges in batches:

  1. Job start: hold total_records × max_per_record × 1.25 (25% safety buffer).
  2. Per record completion: pending credits accumulate.
  3. Every 100 records: pending → charged from the hold.
  4. Job complete: remaining pending charged, unused hold released back to balance.

Use GET /enrichment/jobs/{job_id} to monitor pending vs charged credits mid-run.

Pricing examples

ScenarioCost
Enrich a contact, Apollo finds work email on first hit1 credit
Enrich a contact, no provider matches0 credits
Enrich a contact, need both work email + direct phone, Apollo matches both1 + 2 = 3 credits
Re-enrich the same contact within 7 days0 credits (cache hit)
Bulk enrich 1,000 contacts, ~70% find rate, avg 1.2 credits per hit~840 credits
Build with graph8