Skip to content

Contacts API

The Contacts API enables searching for business contacts/personas using vector search, with support for similarity matching, filtering, and name-based lookup.

The primary endpoint is POST /v1/contacts/discover. The legacy GET /v1/contacts endpoint is still functional and documented at the end of this page for existing integrations.

POST https://api.discolike.com/v1/contacts/discover

Search for business contacts and return them grouped by company domain, with per-company firmographic data on each entry. Billed additively: net-new contacts plus net-new firmo domains for any company surfaced for the first time in the 90-day cache window. The two counters share the same credit pool.

JSON body. Every parameter documented below is accepted as a JSON field.

ParameterDescription
persona_idUse one or more persona IDs as the basis for similarity matching. Up to 10 IDs can be provided.
icp_promptNatural language ICP description. Automatically extracts structured contact filters (seniority, department, industry, country, employee/revenue range, title keywords) and cleans the semantic description. When the query requires company-level signals (tech stack, funding, product descriptions), automatically chains to company discovery to find matching companies first, then searches contacts there. One parameter does everything, and is preferred over setting individual filters manually.
icp_textNatural language description of your ideal contact profile for semantic matching. Does NOT extract filters. Use icp_prompt instead for automatic extraction.
domainLimit results to contacts at specific domains.
inclusion_query_idInclude results from saved queries. Works with domain lists (scopes to companies) and contact lists (scopes to specific contacts).
exclusion_query_idExclude results from saved queries. Works with domain lists (excludes companies) and contact lists (excludes specific contacts).
ParameterDescription
seniorityFilter by seniority level. Acceptable values: executive, vp, director, manager, senior_ic, mid_level, entry_level
negate_seniorityExclude contacts with specified seniority levels.
departmentFilter by department. Must be one of the 15 canonical values exactly (case-sensitive): Operations, Executive, Technology, Sales - Marketing, Finance, Legal, Human Resources, Medical - Science, Customer Service, Research & Development, Administration, Public Relations, Investor Relations, Pro Services, Other. Any other value returns 422. Use icp_prompt if you want natural-language extraction (e.g. “engineering leaders” → Technology).
negate_departmentExclude contacts in specified departments. Same 15 canonical values as department.
skillsFilter by skills. Multiple skills can be provided.
nameFilter by contact name (partial match supported).
titleFilter by job title. Each item is a separate match term. Supports quoted phrases and + prefix for required terms. C-suite acronyms are auto-expanded to their spelled-out forms and vice versa (e.g. CEO also matches Chief Executive Officer); wrap a term in quotes to match it literally without expansion.
negate_titleExclude contacts with specified job titles. C-suite acronyms are expanded the same way as in title.
summaryFilter by profile summary text (semantic search).
negate_summaryExclude contacts matching this summary description.
person_countryFilter by contact’s country using ISO-3166-1 alpha-2 codes. Also accepts region codes: ANZ, APAC, ASEAN, BENELUX, CEE, DACH, EMEA, EU, GCC, LATAM, MENA, NORDICS.
negate_person_countryExclude contacts in specified countries.
person_stateFilter by contact’s state/region. Accepts ISO 3166-2 codes (e.g. CA, BY) or full names (e.g. California, Bayern). Resolved against every selected person_country (region aliases are expanded first), so multiple countries are allowed and an ambiguous name is kept for each country it resolves under. A value that resolves under none of them is dropped rather than rejected. See States for supported countries.
ParameterDescription
has_emailSet to true to only include contacts with email addresses.
email_validatedSet to true to only include contacts with validated email addresses. The email_validated flag reflects an SMTP/deliverability check on the address itself.
has_phoneSet to true to only include contacts with phone numbers.
has_mobileSet to true to only include contacts whose phone array contains at least one mobile number.
has_linkedinSet to true to only include contacts with LinkedIn profiles.
min_connectionsMinimum LinkedIn connections required.
jobstart_dateFilter by job start date: a minimum date (YYYY-MM-DD) or a range (YYYY-MM-DD,YYYY-MM-DD). Matches contacts who started their current role in this window, the “recently hired” filter. Contacts without a known start date are excluded.
ParameterDescription
filter_industryFilter by company industry. Uppercase enum values (e.g. SAAS, SOFTWARE, FINANCIAL_SERVICES, HEALTHCARE, CLOUD_COMPUTING). See BizData Dataset for the full enum.
negate_filter_industryExclude contacts at companies in specified industries.
filter_countryFilter by company country using ISO-3166-1 alpha-2 codes. Also accepts the same region aliases as person_country.
negate_filter_countryExclude contacts at companies in specified countries.
filter_stateFilter by company state/region. Accepts ISO 3166-2 codes (e.g. CA, BY) or full names (e.g. California, Bayern). Resolved against every selected filter_country (region aliases are expanded first), so multiple countries are allowed and an ambiguous name is kept for each country it resolves under. A value that resolves under none of them is dropped rather than rejected. See States for supported countries.
negate_filter_stateExclude contacts at companies in specified states (same format as filter_state).
employee_rangeFilter by company size. Format: 'min,max' (e.g. '51,200'). Maps to buckets: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+.
revenue_rangeDeprecated, no-op. Revenue is a company-level attribute. Filter on /discover by revenue_range and pass the resulting domains to /contacts/discover. The output field is still populated on each result.
ParameterDescription
max_recordsMaximum contacts to return, range 20-10,000 (optional, defaults to 100). Use this for a literal total-contact cap. Do not combine with max_companies.
max_companiesMaximum enriched companies to return, range 1-10,000. Use this when you want the response to stop after up to N company/domain entries. Do not combine with max_records. When paired with results_by_company=0 (no per-company cap), the total is capped at 10,000 contacts.
offsetRecords to skip for pagination (optional, defaults to 0).
results_by_companyMax contacts per company domain, range 0-100 (optional, defaults to 5). With max_companies, controls how many contacts can be returned under each enriched company. Example: max_companies=100, results_by_company=2 → up to 100 companies with up to 2 contacts each. Set results_by_company=0 to remove the per-company cap. With max_records, the total is capped at max_records; with max_companies, the total is capped at 10,000. Any non-zero value forces offset to 0.
include_search_contactsSet to true to include input persona_ids in results (optional, defaults to false).
consensusNumber of top results to use for building search vector, range 1-20 (optional, defaults to 1).
Terminal window
curl -X POST https://api.discolike.com/v1/contacts/discover \
-H "x-discolike-key: API_KEY" \
-H "Content-Type: application/json" \
-d '{
"icp_prompt": "VP of Sales at SaaS companies",
"filter_industry": ["SAAS"],
"max_companies": 50,
"results_by_company": 5
}'
{
"results": {
"stripe.com": {
"domain": "stripe.com",
"name": "Stripe",
"employees": "10001+",
"revenue_range": ">1B",
"industry_groups": {"SAAS": 0.81, "SOFTWARE": 0.1},
"address": {"country": "US", "state": "California"},
"email_pattern": "{first_name}@stripe.com",
"email_pattern_confidence": 59,
"email_pattern_guess": "marissa@stripe.com",
"contacts": [
{
"persona_id": 319675651,
"domain": "stripe.com",
"name": "Marissa Fagnani",
"title": "Delivery Manager",
"department": "Technology",
"seniority": "manager",
"skills": ["program management", "delivery"],
"phone": [
{"phone": "+17406072462", "type": "mobile"},
{"phone": "+14197337789", "type": "mobile"}
],
"email": "marissa@stripe.com",
"email_validated": true,
"social_urls": ["https://www.linkedin.com/in/marissa-fagnani"],
"connections": 500,
"country": "US",
"state": "OR",
"industry": ["SAAS", "SOFTWARE"],
"employees": "10001+",
"revenue_range": ">1B",
"jobstart_date": "2025-11-01",
"similarity": 87,
"summary": null,
"company_name": "Stripe"
}
]
}
},
"total_contacts": 142,
"total_domains": 38
}
  • The company-level address.state is the ISO 3166-2 subdivision name (e.g. California), while the persona-level state on each contact is the raw subdivision code (e.g. OR). Both appear in the same response.
  • phone is an array of objects: [{"phone": "+E164", "type": "mobile" | "landline"}, ...]. A contact can have multiple entries. Type is re-derived per call from libphonenumber, so it is always populated when a phone is present.
  • industry is an array of uppercase enum strings, e.g. ["SAAS", "SOFTWARE"]. See BizData Dataset for the full enum.
  • department is one of the 15 values listed under Persona Filters.
  • social_urls may include LinkedIn and X/Twitter URLs.

The endpoint supports three modes, selected by which parameters you provide:

  1. Similarity mode: when persona_id, icp_text, or icp_prompt is provided. Uses vector search with cosine similarity scores (0-100 scale on the similarity field).
  2. Domain mode: when only domain is provided. Returns all contacts at the specified domains matching filters.
  3. Filter mode: when no search input is provided. Returns contacts matching the company and persona filters.
POST https://api.discolike.com/v1/contacts/discover/generate

Discovers candidate contacts at a list of companies using your own search provider (BYOS) and either your own LLM (BYOK) or DiscoLike Groove, the native extractor (no LLM key). Runs through the DiscoGen pipeline with web search always enabled. Returns a task ID immediately; poll GET /discogen/status/{task_id} for per-domain contact rows plus the company’s inferred email pattern.

A search provider is required either way. Without one the request returns 400.

No platform billing per contact returned, and the request count is the same for both engines. Results may include hallucinated personas, so treat email, linkedin_url, and other identifying fields as candidate data until validated.

integration_id picks the engine:

ValueEngineBehavior
An LLM integration UUIDYour LLM (BYOK)Extracts contacts and validates each title against icp_text. Status reports title_validation: "llm"
"native"DiscoLike Groove (discolike/groove)No LLM key needed. Extracts every person the search surfaces at the company together with a title. No title validation against icp_text, so every row comes back with match_status: "match". Status reports title_validation: "none"
OmittedYour default LLM integration, or native when you have noneMatches whichever engine it resolves to

icp_text still shapes the search queries on the native engine, so it stays worth writing well. Native titles are best effort: filter them yourself when titles must match the ICP exactly.

The other DiscoGen endpoints accept UUIDs only. Sending "native" to them returns 400.

ParameterTypeRequiredDescription
icp_textStringYesPersona description (e.g., “VPs or Directors of Marketing at B2B SaaS”). Must be non-empty
domainsArrayYesList of company domains to search at (1-10,000)
context_modeStringNoWhat data the LLM sees: website (default), profile, domain
integration_idStringNoLLM provider integration UUID, or native for keyless extraction without title validation. Omit for your default LLM integration, or native when you have none
search_provider_idStringNoSearch provider UUID. Uses your default if omitted. Required at the org level
search_context_sizeStringNoSearch depth per company: low (default), medium, high
max_contacts_per_domainIntegerNoCap on contacts returned per company (default 10)
find_emailsBooleanNoDefault false. When true, every row that has a name but no email is run through the email finder before the task completes: email is filled where an address was proven and email_status carries the finder verdict. Found addresses bill under the finder’s rules (90-day dedupe); nothing else on the run bills

Web search is always enabled server-side, because contact discovery depends on LinkedIn/company-site lookups.

Without find_emails, email is null unless a source shows the address verbatim, so the task returns candidate names, titles and LinkedIn URLs. To find addresses afterwards instead, build first_name,last_name,domain rows from the result and submit them to POST /email/find/batch.

{
"task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"column_name": ["name", "title", "department", "seniority", "email", "linkedin_url", "skills", "phone", "email_pattern", "email_pattern_confidence", "email_pattern_guess"],
"status": "in_progress",
"total_domains": 2
}

Each domain yields {"contacts": [...], "email_pattern": ..., "email_pattern_confidence": ..., "email_pattern_guess": ...}: zero or more contact rows plus the company-wide email pattern, once per domain (pattern fields are omitted when the indexer has none):

ColumnTypeNotes
nameStringFull name of the contact
titleString|nullJob title
departmentString|nullOne of the 15 canonical department values (see Persona Filters)
seniorityString|nullexecutive, vp, director, manager, senior_ic, mid_level, entry_level
emailString|nullOnly if a source shows it verbatim, or proven by the finder when find_emails is set
email_statusString|nullOnly with find_emails: the finder verdict for a row it was run on: found, not_found, catch_all, catch_all_pattern, undeliverable, inconclusive, timeout, greylisted, rate_limited, error. Absent on rows that already had an email or had no usable name
linkedin_urlString|nullLinkedIn profile URL
skillsArray|nullSkills attributed to the contact
phoneArrayArray of phone objects with phone (+E164) and type (mobile or landline) fields, same shape as the indexed /contacts/discover response.
email_patternString|nullTemplate, e.g. first.last@, flastname@
email_pattern_confidenceNumber|null0-1
email_pattern_guessString|nullExample email for a hypothetical John Smith
Terminal window
curl -X POST https://api.discolike.com/v1/contacts/discover/generate \
-H "x-discolike-key: API_KEY" \
-H "Content-Type: application/json" \
-d '{
"icp_text": "VPs or Directors of Marketing at B2B SaaS",
"domains": ["gusto.com", "rippling.com"]
}'

Run the same list on the native engine, with no LLM key:

Terminal window
curl -X POST https://api.discolike.com/v1/contacts/discover/generate \
-H "x-discolike-key: API_KEY" \
-H "Content-Type: application/json" \
-d '{
"icp_text": "VPs or Directors of Marketing at B2B SaaS",
"domains": ["gusto.com", "rippling.com"],
"integration_id": "native"
}'

Poll for results:

Terminal window
curl https://api.discolike.com/v1/discogen/status/TASK_ID \
-H "x-discolike-key: API_KEY"

The completed status payload carries title_validation, "llm" or "none", telling you whether titles were validated against icp_text.

GET https://api.discolike.com/v1/contacts/count

Accepts the same filter parameters as the Discover Contacts endpoint (persona filters, contact data filters, and company filters). Useful for sizing a result set before paying to retrieve it.

Terminal window
curl "https://api.discolike.com/v1/contacts/count?seniority=vp&filter_industry=SAAS&filter_country=US" \
-H "x-discolike-key: API_KEY"
{
"count": 15432
}
GET https://api.discolike.com/v1/contacts/lookup
ParameterDescription
persona_idThe persona ID to look up.
linkedinLinkedIn URL or username to look up. Must be a valid LinkedIn profile URL or a bare username.
emailEmail address to look up (exact match, case-insensitive). Must be a valid email address.

One of persona_id, linkedin, or email must be provided. Returns 404 when no contact matches.

Terminal window
curl "https://api.discolike.com/v1/contacts/lookup?persona_id=12345678" \
-H "x-discolike-key: API_KEY"

Or lookup by LinkedIn URL/username:

Terminal window
curl "https://api.discolike.com/v1/contacts/lookup?linkedin=https://www.linkedin.com/in/janedoe" \
-H "x-discolike-key: API_KEY"

Or lookup by email:

Terminal window
curl "https://api.discolike.com/v1/contacts/lookup?email=jane.doe@example.com" \
-H "x-discolike-key: API_KEY"
{
"persona_id": 12345678,
"name": "Jane Doe",
"title": "VP of Sales",
"domain": "example.com",
"company_name": "Example Corp",
"summary": "Experienced sales leader with 15+ years in enterprise software..."
}
GET https://api.discolike.com/v1/contacts

Same filter vocabulary as POST /v1/contacts/discover, passed as query parameters instead of a JSON body. For repeated values (e.g., multiple domains, seniority levels), repeat the query parameter: ?domain=a.com&domain=b.com&seniority=vp&seniority=director. For result limits, pass either max_companies or max_records, not both.

Terminal window
curl "https://api.discolike.com/v1/contacts?icp_text=VP%20of%20Sales%20at%20SaaS%20companies&seniority=vp&filter_industry=SAAS&max_companies=50&results_by_company=5" \
-H "x-discolike-key: API_KEY"
[
{
"persona_id": 319675651,
"domain": "stripe.com",
"name": "Marissa Fagnani",
"title": "Delivery Manager",
"department": "Technology",
"seniority": "manager",
"skills": ["program management", "delivery"],
"phone": [
{"phone": "+17406072462", "type": "mobile"},
{"phone": "+14197337789", "type": "mobile"}
],
"email": "marissa@stripe.com",
"email_validated": true,
"social_urls": ["https://www.linkedin.com/in/marissa-fagnani"],
"connections": 500,
"country": "US",
"state": "OR",
"industry": ["SAAS", "SOFTWARE"],
"employees": "10001+",
"revenue_range": ">1B",
"jobstart_date": "2025-11-01",
"similarity": 87
}
]