---
title: "Enrich a list's contacts in place"
method: POST
path: "/business/lists/{listId}/enrich"
tags: ["Lists"]
---

# Enrich a list's contacts in place

`POST /business/lists/{listId}/enrich`

## Request

- Base URL: `https://api.tryfuse.ai/api/v1`
- URL: `https://api.tryfuse.ai/api/v1/business/lists/{listId}/enrich`
- Auth: HTTP basic

Runs an enrichment over the list's contacts in place. `mode` selects what runs: the waterfall modes (`email`, `phone`, `phone_and_email`) discover new values through the provider waterfall and take an optional row `filters` object or an explicit `contactIds` selection; `validate_emails` re-verifies the contacts' existing emails; `demographics` resolves manually-added rows to canonical contacts. `limit` is how many rows are taken, from `offset` (default 0) in the list's default order after `filters` — required unless `contactIds` is given, and capped at 5,000 for `validate_emails`/`demographics`. Rows that already hold the requested data, or were searched for it before, are skipped and not charged but still count toward `limit`, so a repeated call with the same `offset` and `limit` takes the same rows: pass a larger `offset` to reach rows further down. A call identical to one whose job is still queued or running (same `mode`, `filters`, `offset` and `limit`) returns that job instead of starting another. Billing is per processed contact: up to 50 credits per work email (100 when personal emails are prioritised in the workspace settings), 200 per phone, 10 per re-verified address, 2 per resolved demographics row; the balance must cover the reserved price up front (402 INSUFFICIENT_CREDITS) and contacts already carrying the requested data are never re-charged — with the waterfall modes a fully enriched list answers 409 ALL_CONTACTS_ALREADY_ENRICHED before anything bills, while `validate_emails` and `demographics` report a nothing-to-do run as 202 with `totalContacts: 0`. Async: `totalContacts` is the number of rows this run takes (at most `limit`, fewer at the list's end; a call that returns a running job, or explicit `contactIds` that join a queued one, answers that job's total) and `estimatedMinutes` a rough duration; poll `GET /business/lists/{listId}/rows` (emails/phones fill in) or the list's `metrics`. The waterfall modes run as a job: the 202 also carries its `jobId` and `status` (`pending` while it waits behind your other enrichment job, `processing` once it runs); poll `GET /business/jobs/{jobId}` for its status and counts. A job whose balance runs out mid-run pauses (`stopped`, with `pausedReason` INSUFFICIENT_CREDITS) and is resumed from the app. To work through a list in batches, pass `offset` as the previous call's `offset` plus `limit`: a call without `offset` always starts at the list's first row.

Errors:
- `402` `INSUFFICIENT_CREDITS` — The credit balance does not cover the reserved price for the requested rows.
- `404` `LIST_NOT_FOUND` — No list with that id is visible to the token owner.
- `409` `ALL_CONTACTS_ALREADY_ENRICHED` — Every contact in the list already carries the requested data; nothing to run. Waterfall modes (email, phone, phone_and_email) only — validate_emails and demographics answer 202 with totalContacts 0 instead.
- `422` `CONTACT_LIST_REQUIRED` — The list is a company list; enrichment runs on contacts.
- `422` `CONTACT_NOT_IN_LIST` — A contactIds entry is not a row of this list.
- `422` `VALIDATION_FAILED` — mode missing/unknown, limit missing without contactIds or outside 1-10000, filters/contactIds sent together or with validate_emails/demographics, offset sent with contactIds or with validate_emails/demographics.
- `429` `RATE_LIMITED` — The lists rate bucket (120 requests/minute, 10,000/day per token owner) is exhausted.

Requires one of the following token scopes: lists.

## Path parameters

- `listId` string, required: 24-character hex id of the list (from GET /business/lists).

## Request body

- object
  - `mode` 'email' | 'phone' | 'phone_and_email' | 'validate_emails' | 'demographics', required: What to run: email, phone or phone_and_email discover new values through the enrichment waterfall (billed per contact processed, at up to 50 credits per work email — 100 when personal emails are prioritised — and 200 per phone); validate_emails re-verifies the contacts' existing emails (10 credits per address); demographics resolves manually uploaded rows to canonical contacts (2 credits per resolved contact).
  - `offset` integer: Rows to skip before taking limit of them (waterfall modes only, default 0). A repeated call with the same offset and limit takes the same rows; pass a larger offset to reach rows further down the list. Not allowed with contactIds.
  - `filters` object: Row-filter object narrowing which contacts the waterfall modes process, in the same language as the filters query of GET /business/lists/{listId}/rows; not allowed for validate_emails or demographics and mutually exclusive with contactIds. Text keys (name, title, dept, level, industryName, country, state, personalLocation, companyName, companyIndustry, companyHeadQuartersCountry, companyLocation, website, campaignEngagement, otherLists) take an array of accepted values, e.g. {"companyName":["pomelo"]} or {"country":["argentina"]}, or {"include":[...],"exclude":[...]} — otherLists takes list NAMES, not ids; numberOfEmployees takes headcount ranges the same way but as STRINGS ("51-200", "10001+" or a bare head count "500" — an unquoted JSON number answers 500) and revenue a plain number; keywords (a wildcard match on the job title only) and linkedinUrl take one string; enrichedEmail, enrichedPhone and emailValidity take {"include":[...]} of their statuses; icpMatch takes ["yes"] or ["no"]; customColumns maps a column id to {"dataType":"string","include":[...]}, {"dataType":"boolean","value":true} or {"dataType":"number","rangeMin":1,"rangeMax":10}. Company lists honour companyName, companyIndustry, companyHeadQuartersCountry, website, numberOfEmployees, campaignEngagement and customColumns. Every key is optional; an unknown key or a wrong value shape is rejected rather than ignored. Values inside one key are OR-joined and different keys are AND-ed. VALUES ARE NOT VALIDATED: level, industryName, country, state, website and companyHeadQuartersCountry are matched as whole values and campaignEngagement, icpMatch and enum cells as exact strings, so a value in the wrong vocabulary is accepted and silently matches nothing — each key's own description names its vocabulary and the endpoint that serves it. A rejected filter answers 422 VALIDATION_FAILED.
    - `name` union: Contact full name (contact lists). Free text: each value is matched as a case-insensitive substring of firstName + space + lastName, so Ana matches Ana Silva.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `title` union: Job title (contact lists). Free text: each value is matched as a case-insensitive substring of the stored job title, so vp also matches VP of Sales. There is no fixed vocabulary — `GET /business/prospects/autocomplete?field=title&q=` suggests the titles the enrichment provider knows.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `dept` union: Department (contact lists). Matched as a case-insensitive substring of the contact's department, whose values come from the provider's job_title_role vocabulary — take them from `GET /business/prospects/filter-options` (field `job_title_role`: sales, engineering, human_resources, …). A contact with several roles stores them comma-joined, which a substring match still finds. Any other text is accepted and matches only rows that literally contain it.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `level` union: Seniority level (contact lists). Matched EXACTLY (case-insensitive, whole value) against the contact's level, whose values come from the provider's job_title_levels vocabulary — take them from `GET /business/prospects/filter-options` (field `job_title_levels`): cxo, director, entry, manager, owner, partner, senior, training, unpaid, vp. A contact carrying several levels stores them comma-joined (cxo, owner) and an exact match on one level does NOT find it; any other value is accepted and matches nothing.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `industryName` union: Contact's industry (contact lists). Matched EXACTLY (case-insensitive, whole value) against the industry on the contact, which the enrichment provider fills from its own 147-value list — take the values from `GET /business/prospects/filter-options` (field `industry`, e.g. computer software, hospital & health care). A near-miss such as Software is accepted and matches nothing.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `country` union: Contact's country (contact lists). Matched EXACTLY (case-insensitive, whole value) against the country of the contact's address, stored as the provider spells it (united states, not US) — take the values from `GET /business/prospects/filter-options` (field `location_country`). An ISO code or another spelling is accepted and matches nothing.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `state` union: Contact's state or region (contact lists). Matched EXACTLY (case-insensitive, whole value) against the state of the contact's address — the provider's region name, e.g. california. There is no enum; `GET /business/prospects/autocomplete?field=region&q=` suggests from the same region vocabulary. A spelling no row carries is accepted and matches nothing.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `personalLocation` union: Contact's location text (contact lists). Each value is matched as a case-insensitive substring against the contact's city, state AND country separately, and the row matches when any one of the three contains it — so pass ONE component (buenos aires, or california, or united states), never a whole line like san francisco, california, united states, which matches no component. No endpoint serves the city names: read the stored components off the rows themselves, where `GET /business/lists/{listId}/rows` returns each row's `location` as {city, state, country}. For the other two components, `GET /business/prospects/autocomplete?field=region&q=` suggests the state/region names and `GET /business/prospects/filter-options` (field `location_country`) lists the country names. Do NOT feed this key from `GET /business/prospects/autocomplete?field=location`: that endpoint returns whole city, state, country lines, which are exactly the shape that matches no component.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `companyName` union: Company name (both list kinds). Free text: each value is matched as a case-insensitive substring of the company name. On a company list, `GET /business/lists/{listId}/filter-values?field=name` returns the exact names that list holds.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `companyIndustry` union: Company industry (both list kinds). Matched as a case-insensitive substring of the company's industry, filled by the enrichment provider from its own list — take the values from `GET /business/prospects/filter-options` (field `job_company_industry`). Do not use the company-search vocabulary from /business/companies/filter-options: that is a different provider's list and its values are not what these rows store.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `companyHeadQuartersCountry` union: Company headquarters country (both list kinds). Matched EXACTLY (case-insensitive, whole value), but each value is first expanded to every spelling of that country (full name, ISO-3166 alpha-2 and alpha-3), because the field mixes both vocabularies — so the full country name alone is enough. Take the names from `GET /business/prospects/filter-options` (field `location_country`); a value that resolves to no country is matched verbatim.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `companyLocation` union: Company location text (contact lists). Each value is matched as a case-insensitive substring against the company address's city, state AND country separately, and the row matches when any one of the three contains it, so pass ONE component. Many companies are stored with a country and nothing else, in which case a city or state value is accepted and matches nothing; `GET /business/prospects/filter-options` (field `location_country`) lists the country names.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `website` union: Company web domain (both list kinds). Matched EXACTLY (case-insensitive, whole value) against the stored domain, so pass the BARE domain — brightpay.com. A full URL (https://brightpay.com/), a www. prefix or a partial domain is accepted and matches nothing. On a company list, `GET /business/lists/{listId}/filter-values?field=domain` returns the exact domains that list holds.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `numberOfEmployees` union: Company size (both list kinds). Each value is parsed as a headcount range and matched against the company's LinkedIn head count; accepted values are a range ("51-200"), an open top end ("10001+") or a bare head count ("500", meaning exactly 500). Send every value as a JSON STRING: an unquoted JSON number passes validation and then throws while the range is parsed, so the call answers 500 UNEXPECTED_ERROR instead of filtering. The provider's own labels — 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+, served by `GET /business/prospects/filter-options` (field `job_company_size`) — all parse. Anything else is read with parseInt, so a value that STARTS with digits silently narrows to that one number ("51 to 200" matches companies with exactly 51 employees, it is not ignored) and a value that does not ("Enterprise") is silently dropped; if every value is dropped nothing is left to match and the call answers zero rows rather than ignoring the filter, and companies with no stored head count never match.
      - union[]: Accepted range labels, OR-joined: a row matches when any one of them matches (at most 200).
        - union
          - string
          - number
      - object
        - `include` union[]: Range labels to keep, OR-joined: a row matches when any one of them matches (at most 200).
          - union
            - string
            - number
        - `exclude` union[]: Range labels to drop: a row is dropped when it matches any one of them (at most 200).
          - union
            - string
            - number
    - `revenue` union: Company revenue (contact lists). Accepted values: a plain number, matched for exact equality against the company's stored revenue. The enrichment provider stores revenue as a LABEL (for example $10m-$25m), and a label is coerced to a number before matching, so a label — or any other non-numeric value — is accepted and matches nothing. Use numberOfEmployees to narrow by company size instead.
      - union[]: Accepted range labels, OR-joined: a row matches when any one of them matches (at most 200).
        - union
          - string
          - number
      - object
        - `include` union[]: Range labels to keep, OR-joined: a row matches when any one of them matches (at most 200).
          - union
            - string
            - number
        - `exclude` union[]: Range labels to drop: a row is dropped when it matches any one of them (at most 200).
          - union
            - string
            - number
    - `keywords` string: Wildcard search over the contact's job title only (contact lists) — it matches no other field. Free text: every character is matched literally except the two wildcards, `*` for any run of characters and `?` for any single one, the match is case-insensitive and unanchored (vp matches VP of Sales), and the regex characters . ^ $ + ( ) [ ] { } | \ are rejected with 422.
    - `linkedinUrl` string: LinkedIn URL (contact lists). One value, matched as a case-insensitive substring of the stored URL, which is kept scheme-less and www-less as linkedin.com/in/<slug> — so pass the slug or linkedin.com/in/<slug>. A full https://www.linkedin.com/in/<slug> is longer than the stored value, so it is accepted and matches nothing.
    - `enrichedEmail` object: Email enrichment state (contact lists). One of exactly: enriched (an email was found), not_enriched (never attempted) or no_emails_found (attempted, nothing found).
      - `include` string[]: Statuses to keep: enriched, not_enriched, no_emails_found. A contact is kept when it is in any one of the listed states (OR-joined), so listing every state keeps every row.
      - `exclude` string[]: Statuses to drop: enriched, not_enriched, no_emails_found. The states are OR-joined: a contact in any one of them is dropped.
    - `enrichedPhone` object: Phone enrichment state (contact lists). One of exactly: enriched (a phone was found) or not_enriched (no phone yet).
      - `include` string[]: Statuses to keep: enriched, not_enriched. A contact is kept when it is in any one of the listed states (OR-joined), so listing both states keeps every row.
      - `exclude` string[]: Statuses to drop: enriched, not_enriched. The states are OR-joined: a contact in any one of them is dropped.
    - `emailValidity` object: Validation status of the contact's emails (contact lists). One of exactly: valid, invalid, catch-all, unknown, do_not_mail, personal, unverified — valid means a deliverable WORK address (personal ones are excluded from it), personal means a deliverable personal address, and unverified means a user-uploaded address that was never verified.
      - `include` string[]: Statuses to keep: valid, invalid, catch-all, unknown, do_not_mail, personal, unverified. OR-joined over statuses and over the contact's addresses: a contact is kept when any one of its emails carries any one of the listed statuses.
      - `exclude` string[]: Statuses to drop: valid, invalid, catch-all, unknown, do_not_mail, personal, unverified. OR-joined over statuses and over the contact's addresses: a contact is dropped when any one of its emails carries any one of the listed statuses.
    - `icpMatch` union: ICP match outcome (contact lists). Accepted values: exactly one of yes (rows matching the ICP saved in the app) or no (the rest). When the workspace has no ICP configured nothing is a match, so yes returns no rows at all and no returns every row.
      - string[]: Accepted values: exactly one of ["yes"] (ICP-matched rows) or ["no"] (the rest); ["yes","no"] is read as ["yes"].
      - object
        - `include` string[]
        - `exclude` string[]
    - `campaignEngagement` union: Campaign engagement bucket (both list kinds). One of exactly: contacted (at least one campaign email has been sent to the contact), in_active_campaign (targeted by a running campaign, nothing sent yet) or never_engaged (neither) — compared as whole values, case included, so any other value is accepted and returns no rows. A company's bucket is the highest bucket among its contacts.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `otherLists` union: NAMES (not ids) of other lists the contact must — or with exclude, must not — also belong to (contact lists). Each value is matched as a case-insensitive substring of the list names from `GET /business/lists`; only the team's static contact lists are searched, so the list being read, dynamic lists and archived lists never satisfy it.
      - string[]: Accepted values, OR-joined: a row matches when any one of them matches (1-200 values, max 500 characters each).
      - object: Explicit include/exclude lists; include is OR-joined, exclude drops a row matching any of its values.
        - `include` string[]: Values to keep, OR-joined: a row matches when any one of them matches (up to 200).
        - `exclude` string[]: Values to drop: a row is dropped when it matches any one of them (up to 200).
    - `customColumns` object: Cell filters keyed by custom/smart column id (from `GET /business/lists/{listId}/columns`), both list kinds. Every column listed must match (AND-ed); a column id that is not on this list matches no row.
  - `contactIds` string[]: Explicit contacts to process (24-character hex ids, 1-500) for the waterfall modes. Each must be a row of this list (a row `id` from GET /business/lists/{listId}/rows); any other id answers 422 CONTACT_NOT_IN_LIST. Not allowed for validate_emails or demographics and mutually exclusive with filters.
  - `limit` integer: How many rows to take, starting at offset, in the list's default order (oldest first) after filters (1-10000); required unless contactIds is given, and validate_emails/demographics cap it at 5000. Rows that already hold the requested data, or were searched for it before, are skipped and not charged but still count toward limit.

## Response `202`

Success

- object
  - `listIds` string[], required
  - `mode` 'email' | 'phone' | 'phone_and_email' | 'validate_emails' | 'demographics', required
  - `totalContacts` integer, required
  - `estimatedMinutes` integer, required
  - `jobId` string: Waterfall modes only: the run's job, to poll at GET /business/jobs/{jobId}.
  - `status` 'pending' | 'processing': Waterfall modes only: pending while the job waits behind your other enrichment job, processing once it runs.

## Other responses

- `400`: Error
- `401`: Error
- `402`: Error
- `403`: Error
- `404`: Error
- `409`: Error
- `422`: Error
- `429`: Error
- `500`: Error

---

[API](https://skmtc.dev/fuseai/apis/fuse-business-api.md) · [All operations](https://skmtc.dev/fuseai/apis/fuse-business-api/llms.txt) · [OpenAPI document](https://skmtc.dev/fuseai/apis/fuse-business-api/revisions/50d95e0536f4?raw)
