Skip to content

Reference

Every method on Discolike, grouped by namespace and sorted alphabetically, followed by the models and exceptions in Types.

New here? Start with Getting Started for install, authentication, the client constructor, and how async, jobs, and errors work.

Every method that sends parameters takes one request model from discolike.requests as its first positional argument; path parameters and file uploads are keyword-only after it. Each model subclasses DiscolikeRequest. The Default column in each table is the model’s declared default, mirroring the server’s: a field you never set is not sent at all, so the server default governs. An explicit None reaches JSON bodies as null and is dropped from query strings. Unknown fields pass through to the wire. A value outside the model’s constraints raises pydantic.ValidationError locally before any request is made. See Request models for the migration from keyword arguments. Every method has an identical twin on AsyncDiscolike.

Account usage and quota. Wraps the Usage endpoint.

client.account.usage() -> Usage

GET /usage. Takes no arguments. Returns Usage.

from discolike import Discolike
with Discolike() as client:
usage = client.account.usage()
print(usage.month_to_date_requests, usage.month_to_date_spend)
26 26.37

Per-domain lookups. domain is required everywhere except extract, which takes either url or domain. Subdomains are normalized to the root domain server-side.

client.companies.data(params: CompaniesDataParams) -> BizData

CompaniesDataParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredDomain to look up

Returns BizData. REST endpoint: BizData.

from discolike.requests import CompaniesDataParams
profile = client.companies.data(CompaniesDataParams(domain="stripe.com"))
print(profile.name, profile.employees, profile.revenue_range)
# Stripe 10001+ >1B
client.companies.score(params: CompaniesScoreParams) -> Score

CompaniesScoreParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredDomain to look up

Returns Score. REST endpoint: Score.

from discolike.requests import CompaniesScoreParams
score = client.companies.score(CompaniesScoreParams(domain="stripe.com"))
print(score.score, score.first_event, score.parameters.base_score)
# 701 2011-01-04 688.0
client.companies.growth(params: CompaniesGrowthParams) -> Growth

CompaniesGrowthParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredDomain to look up

Returns Growth. REST endpoint: Growth.

from discolike.requests import CompaniesGrowthParams
growth = client.companies.growth(CompaniesGrowthParams(domain="stripe.com"))
print(growth.score_growth_3m, growth.subdomain_growth_3m)
# 1.1 7.8
client.companies.extract(params: CompaniesExtractParams) -> ExtractResult

CompaniesExtractParams is imported from discolike.requests.

FieldTypeDefaultDescription
urlstr | NoneNoneURL of the page to extract
domainstr | NoneNoneBare domain, an alias for url="https://{domain}" that hits the cached page when available

Pass one of the two. Returns ExtractResult. REST endpoint: Extract.

from discolike.requests import CompaniesExtractParams
page = client.companies.extract(CompaniesExtractParams(domain="stripe.com"))
print(page.language, page.text[:60])
# en Stripe | Financial Infrastructure to Grow Your Revenue Produ
client.companies.redirects(params: CompaniesRedirectsParams) -> list[Redirect]

CompaniesRedirectsParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredQuery domain
matchLiteral['source', 'linked'] | None'source'Match direction

Returns list[Redirect]. REST endpoint: Redirects.

from discolike.requests import CompaniesRedirectsParams
for row in client.companies.redirects(CompaniesRedirectsParams(domain="stripe.com", match="linked"))[:2]:
print(row.source_domain, "->", row.linked_domain)
client.companies.vendors(params: CompaniesVendorsParams) -> list[Vendor]

CompaniesVendorsParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredQuery domain
matchLiteral['client', 'vendor'] | None'client'Match the domain as client or as vendor

Returns list[Vendor]. REST endpoint: Vendors.

from discolike.requests import CompaniesVendorsParams
for row in client.companies.vendors(CompaniesVendorsParams(domain="stripe.com", match="client"))[:2]:
print(row.client_domain, "->", row.vendor_domain)
client.companies.subsidiaries(params: CompaniesSubsidiariesParams) -> list[Subsidiary]

CompaniesSubsidiariesParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredQuery domain
matchstr | None'parent'parent returns the domain’s subsidiaries, child returns its parent, recursive returns the parent plus all siblings and subsidiaries. source and linked are advanced role-based matches

Returns list[Subsidiary]. REST endpoint: Subsidiaries.

from discolike.requests import CompaniesSubsidiariesParams
for row in client.companies.subsidiaries(CompaniesSubsidiariesParams(domain="stripe.com", match="parent"))[:2]:
print(row.parent_domain, "->", row.child_domain, row.linked_score)
# stripe.com -> stripe.network 0
# stripe.com -> stripecdn.com 686
client.companies.public_links(params: CompaniesPublicLinksParams) -> list[PublicLink]

CompaniesPublicLinksParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainstrrequiredDomain to discover linkage for
sourceLiteral['email', 'social', 'phone']requiredLinkage source

Returns list[PublicLink]. REST endpoint: PublicLink.

from discolike.requests import CompaniesPublicLinksParams
for row in client.companies.public_links(CompaniesPublicLinksParams(domain="stripe.com", source="email"))[:2]:
print(row.linked_domain, row.link_values)

Person-level search, counting, lookup, matching, and generation.

search(), count(), and discover() share one filter set. Their models, ContactsSearchParams, ContactsCountParams, and ContactFilters, carry identical fields; each method says which fields its endpoint honours. Full field semantics (enum values, region aliases, and date formats) live on the Contacts endpoint page; the table below is the SDK signature.

FieldTypeDefaultDescription
icp_promptstr | NoneNoneNatural-language ICP description, up to 4000 characters. Extracts structured filters automatically; filters you set explicitly take precedence over extracted ones
icp_textstr | NoneNoneNatural-language profile description for semantic matching, up to 4000 characters. Does not extract filters
senioritylist[str] | NoneNoneSeniority levels: executive, vp, director, manager, senior_ic, mid_level, entry_level
negate_senioritylist[str] | NoneNoneSeniority levels to exclude. Same values as seniority
departmentlist[str] | NoneNoneDepartments, 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
negate_departmentlist[str] | NoneNoneDepartments to exclude. Same values as department
skillslist[str] | NoneNoneRequired skills
namestr | NoneNoneContact name, partial match
titlelist[str] | NoneNoneJob title match terms. Supports quoted phrases and a + prefix for required terms; C-suite acronyms expand to their spelled-out forms unless quoted
negate_titlelist[str] | NoneNoneJob titles to exclude. Acronyms expand as in title
summarystr | NoneNoneSemantic match against the profile summary
negate_summarystr | NoneNoneSummary description to exclude
person_countrylist[str] | NoneNoneContact country, ISO-3166-1 alpha-2 or a region alias
negate_person_countrylist[str] | NoneNoneContact countries to exclude
person_statelist[str] | NoneNoneContact state/region. Scoped to a single person_country
has_emailbool | NoneFalseOnly contacts with an email address
email_validatedbool | NoneFalseOnly contacts whose email passed a deliverability check
has_phonebool | NoneFalseOnly contacts with a phone number
has_mobilebool | NoneFalseOnly contacts with at least one mobile number
has_linkedinbool | NoneFalseOnly contacts with a LinkedIn profile
min_connectionsint | NoneNoneMinimum LinkedIn connections, 0 or more
jobstart_datestr | NoneNoneMinimum date YYYY-MM-DD, or a range YYYY-MM-DD,YYYY-MM-DD. Contacts without a known start date are excluded
persona_idlist[int] | NoneNonePersona IDs to use as the similarity basis
domainlist[str] | NoneNoneRestrict to contacts at these company domains
filter_industrylist[str] | NoneNoneCompany industries, each one of the industry categories listed on the Industries page
negate_filter_industrylist[str] | NoneNoneCompany industries to exclude. Same values as filter_industry
filter_countrylist[str] | NoneNoneCompany country, ISO-3166-1 alpha-2 or a region alias
negate_filter_countrylist[str] | NoneNoneCompany countries to exclude
filter_statelist[str] | NoneNoneCompany state/region. Scoped to a single filter_country
negate_filter_statelist[str] | NoneNoneCompany states to exclude
employee_rangestr | NoneNoneCompany size as 'min,max', e.g. '51,200'
inclusion_query_idlist[str] | NoneNoneSaved query IDs to include. See queries
exclusion_query_idlist[str] | NoneNoneSaved query IDs to exclude
max_recordsint | None100Maximum contacts to return, 20-10000
max_companiesint | NoneNoneMaximum enriched companies to return, 1-10000. Do not combine with max_records
offsetint | None0Records to skip, 0-10000
results_by_companyint | None5Maximum contacts per company domain, 0-100. 0 removes the per-company cap. A non-zero value forces offset to 0
include_search_contactsbool | NoneFalseInclude contacts from the search index for broader coverage
consensusint | None1Number of query vectors combined for consensus search, 1-20
client.contacts.search(params: ContactsSearchParams) -> list[Contact]

ContactsSearchParams is imported from discolike.requests. The server honours every filter field above plus max_records, max_companies, and offset.

Returns list[Contact], one per person. REST endpoint: GET /contacts.

from discolike.requests import ContactsSearchParams
contacts = client.contacts.search(
ContactsSearchParams(
domain=["stripe.com"], seniority=["executive"], has_email=True, max_records=20
)
)
for person in contacts[:3]:
print(person.persona_id, person.name, person.title, person.email)
# 1321743327 William Nichols Strategy & Operations, Product william@stripe.com
# 1321743234 Tom Silva Sales Leader - Stripe Platforms tom@stripe.com
# 1321743193 Samuel Fuchs Platform Partnerships samuel@stripe.com
client.contacts.count(params: ContactsCountParams) -> Count

ContactsCountParams is imported from discolike.requests. The server honours the filter fields and ignores max_records, max_companies, and offset. Returns a Count with the total at .count. REST endpoint: GET /contacts/count.

from discolike.requests import ContactsCountParams
result = client.contacts.count(ContactsCountParams(seniority=["executive"], domain=["stripe.com"]))
print(result.count)
# 978
client.contacts.lookup(params: ContactsLookupParams) -> Contact

ContactsLookupParams is imported from discolike.requests.

FieldTypeDefaultDescription
persona_idint | NoneNonePersona ID to resolve
linkedinstr | NoneNoneLinkedIn profile URL or username to resolve
emailstr | NoneNoneEmail address to resolve, exact match

Resolves a single person. Returns Contact. REST endpoint: GET /contacts/lookup.

from discolike.requests import ContactsLookupParams
person = client.contacts.lookup(ContactsLookupParams(persona_id=319671825))
print(person.persona_id, person.name, person.title, person.domain)
# 319671825 Patrick Collison CEO stripe.com
client.contacts.match(params: ContactsMatchParams) -> ContactMatchResponse

ContactsMatchParams is imported from discolike.requests.

FieldTypeDefaultDescription
namestrrequiredPerson’s name
company_namestr | NoneNoneCompany name to narrow the search
domainstr | NoneNoneCompany domain to narrow the search
person_countrystr | NoneNoneContact country, ISO-3166-1 alpha-2, to narrow the search
limitint | None10Maximum candidates to return, 1-20

Ranks candidate people for a name. Returns ContactMatchResponse. Server-side constraints on name and limit are on Contact Match.

from discolike.requests import ContactsMatchParams
response = client.contacts.match(ContactsMatchParams(name="Patrick Collison", company_name="Stripe", limit=2))
for hit in response.matches:
print(hit.match_score, hit.name, hit.title, hit.company_name)
# 96.2 Patrick Collison CEO Stripe
# 66.7 Patrick Collison Cofounder Arc Institute
client.contacts.bulk_match(request: BulkContactMatchRequest) -> Job

BulkContactMatchRequest is imported from discolike.requests, as is BulkContactMatchQueryItem for each entry in queries.

FieldTypeDefaultDescription
querieslist[BulkContactMatchQueryItem]requiredMatch queries, 1-10000 items
enrichbool | NoneFalseHydrate full contact data for each match. Costs credits
limitint | None10Maximum candidates per query, 1-20

BulkContactMatchQueryItem:

FieldTypeDefaultDescription
namestr | NoneNonePerson’s name. Required unless email is set
emailstr | NoneNoneEmail address for exact lookup. When found, name and company matching is skipped for this row
company_namestr | NoneNoneCompany name to narrow the search
domainstr | NoneNoneCompany domain to narrow the search
person_countrystr | NoneNoneContact country, ISO-3166-1 alpha-2

Returns a Job in the contactmatch family. REST endpoint: Contact Bulk Match.

from discolike.requests import BulkContactMatchQueryItem, BulkContactMatchRequest
job = client.contacts.bulk_match(
BulkContactMatchRequest(
queries=[
BulkContactMatchQueryItem(name="Jane Doe", company_name="Acme Corp"),
BulkContactMatchQueryItem(name="John Roe", domain="example.com"),
],
enrich=True,
limit=5,
)
)
status = job.wait()
print(status.status, status.results)
client.contacts.discover(request: ContactFilters) -> ContactsDiscoverResponse

ContactFilters is imported from discolike.requests. The grouped-by-company contact endpoint: the server honours every filter field, the three paging fields search() uses, and results_by_company, include_search_contacts, and consensus.

Returns a ContactsDiscoverResponse: a results map of domain → ContactsByCompany (firmographics plus nested contacts), with total_contacts and total_domains counters. REST endpoint: POST /contacts/discover.

from discolike.requests import ContactFilters
response = client.contacts.discover(
ContactFilters(icp_prompt="VP of Sales at SaaS companies", max_companies=50, results_by_company=5)
)
for domain, company in response.results.items():
for person in company.contacts:
print(domain, person.name, person.title)
client.contacts.generate(request: ContactGenerateRequest) -> Job

ContactGenerateRequest is imported from discolike.requests.

FieldTypeDefaultDescription
icp_textstrrequiredNatural-language description of the contacts to generate
domainslist[str]requiredDomains to generate contacts for, 1-10000 items
context_modeLiteral['website', 'profile', 'domain'] | None'website'What the model sees per domain
integration_idstr | NoneNoneLLM provider integration UUID. Your default is used when omitted
search_provider_idstr | NoneNoneSearch provider integration UUID. Your default is used when omitted
search_context_sizeLiteral['low', 'medium', 'high'] | None'low'Search queries per record
max_contacts_per_domainint | None10Cap on contacts generated per domain
max_company_recordsint | NoneNoneCap on companies processed
full_domainslist[str] | NoneNoneAccepted by the platform; undocumented in the spec
partial_domainslist[str] | NoneNoneAccepted by the platform; undocumented in the spec
initial_contact_countsdict[str, int] | NoneNonePer-domain integer map. Accepted by the platform; undocumented in the spec

Returns a Job in the discogen family, not contactmatch. POST /contacts/discover/generate.

from discolike.requests import ContactGenerateRequest
job = client.contacts.generate(
ContactGenerateRequest(
icp_text="Heads of RevOps at payments companies",
domains=["stripe.com", "adyen.com"],
max_contacts_per_domain=3,
)
)
print(job.wait().status)

Runs an LLM prompt against every record in a batch: one prompt, many domains or personas. Wraps the DiscoGen endpoints.

integration_id and search_provider_id values come from llm_providers and search_providers.

client.discogen.process(request: DiscoGenProcessRequest) -> Job

DiscoGenProcessRequest is imported from discolike.requests.

Defaults in the tables below are the model’s declared defaults and mirror the server’s. A field you never set is not sent at all; the server default governs.

FieldTypeDefaultDescription
querystrrequiredThe prompt to run against each record. At least 1 character
domainslist[str]requiredDomains to process, 1-10,000 items
integration_idstr | NoneNoneLLM provider integration UUID. Your default LLM provider is used if omitted
web_searchbool | NoneFalseEnable web search enrichment
context_modestr | None'website'What the model sees per domain: website, profile, or domain
include_x_searchbool | NoneFalseInclude X (Twitter) results for xAI models
search_provider_idstr | NoneNoneSearch provider integration UUID. Your default search provider is used if omitted
search_context_sizestr | None'low'Search queries per record: low, medium, or high
previous_discogen_datadict[str, Any] | NoneNonePassed through to the platform

POST /discogen/process. Returns a Job in the discogen family.

from discolike.requests import DiscoGenProcessRequest
job = client.discogen.process(DiscoGenProcessRequest(
query="What products or services does this company offer?",
domains=["stripe.com", "shopify.com"],
context_mode="website",
))
final = job.wait(timeout=1800, poll_interval=10)
print(final.status, final.results)
client.discogen.process_personas(request: DiscoGenPersonaProcessRequest) -> Job

DiscoGenPersonaProcessRequest is imported from discolike.requests.

Identical to DiscoGenProcessRequest except for two fields:

FieldTypeDefaultDescription
persona_idslist[int]requiredContact record IDs to process, 1-10,000 items
context_modestr | None'profile'Contact context rather than company context: full, company, profile, profile_summary, or name_only

POST /discogen/process-personas. Returns a Job in the discogen family.

from discolike.requests import DiscoGenPersonaProcessRequest
job = client.discogen.process_personas(DiscoGenPersonaProcessRequest(
query="Is this person a likely economic buyer for developer tooling?",
persona_ids=[12345, 67890],
context_mode="full",
))
final = job.wait()
client.discogen.models() -> DiscogenModels

GET /discogen/models. Lists the LLM models available to you, grouped by provider. Takes no arguments and costs nothing. Returns DiscogenModels.

supports_web_search tells you whether you need a search provider: passing web_search=True for a model without built-in web search, with no search provider available, returns 400 from the API and raises ValidationError.

models = client.discogen.models()
print(list(models.models)[:5])
print([m.model_dump() for m in models.models["openai"]][:3])
# ['openai', 'anthropic', 'replicate', 'huggingface', 'together_ai']
# [{'name': 'o4-mini', 'supports_web_search': True}, {'name': 'gpt-5-pro', 'supports_web_search': True}, {'name': 'gpt-5-nano', 'supports_web_search': True}]
client.discogen.job(task_id: str) -> Job
ParameterTypeDescription
task_idstrTask ID to rehydrate. Positional, not keyword

Rehydrates a handle for a DiscoGen task started earlier. No request is made; it only constructs the handle. Returns a Job.

job = client.discogen.job("d3f0c9a1-1f4b-4a67-9a10-6ba0d2f8e7c1")
print(job.status().status)

discover() and count() are convenience forwarders on the client itself, not a client.discovery namespace. Each takes one request model from discolike.requests, DiscoverParams or CountParams, mirroring the backing DiscoveryResource. Field names outside the model pass through to the API untouched; out-of-range values and unknown enum choices raise pydantic.ValidationError locally before any request is made.

They wrap the Discover and Count endpoints.

client.discover(params: DiscoverParams) -> list[Company]

DiscoverParams is imported from discolike.requests.

At least one discovery input, or one structured filter, is needed for a meaningful query; they can be combined. Every field is optional. The Default column is the model’s declared default, mirroring the server’s; a field you never set is not sent, and the server default governs.

Discovery input:

FieldTypeDefaultDescription
domainlist[str] | NoneNoneExample domains for lookalike matching, up to 10
exclude_domainlist[str] | NoneNoneHard-exclude specific domains from results without affecting lookalike matching, up to 100
icp_textstr | NoneNoneNatural-language description of your ideal customer profile, 3-4000 characters
icp_promptstr | NoneNoneNatural-language ICP description that auto-extracts filters, generates cleaned ICP text, and suggests lookalike domains in one call, up to 4000 characters. Overrides auto_icp_text and auto_phrase_match

Phrase matching:

FieldTypeDefaultDescription
phrase_matchlist[str] | NoneNoneExact text fragments to match in site content, up to 20, each at least 3 characters
negate_phrase_matchlist[str] | NoneNoneExact text fragments to exclude, up to 20, each at least 3 characters

Location:

FieldTypeDefaultDescription
countrylist[str] | NoneNoneISO-3166-1 alpha-2 codes (US, GB, DE). Also accepts region codes that expand to member countries: ANZ, APAC, ASEAN, BENELUX, CEE, DACH, EMEA, EU, GCC, LATAM, MENA, NORDICS
negate_countrylist[str] | NoneNoneCountries to exclude, same codes and region aliases as country
statelist[str] | NoneNoneISO 3166-2 codes (CA, BY) or full names (California, Bayern), up to 100. Scoped to a single country per request; passing more than one country value rejects state. See States
negate_statelist[str] | NoneNoneStates to exclude, same format as state, up to 100

Company filters:

FieldTypeDefaultDescription
categorylist[str] | NoneNoneIndustry filter (SOFTWARE, HEALTHCARE, …), validated locally against the industry list. See Industries
negate_categorylist[str] | NoneNoneIndustries to exclude, same values as category
employee_rangestr | NoneNonemin,max string, e.g. "51,200". Open-ended forms "50," and ",50" are accepted. Maps to buckets 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+
revenue_rangestr | NoneNonemin,max in raw integers, e.g. "1000000,10000000". + suffix on the max is unbounded ("1000000000+"). Maps to buckets <1M, 1-10M, 10-100M, 100M-1B, >1B; unknown-revenue domains are included when the range covers <1M
business_modellist[str] | NoneNoneB2B, B2C, B2G, G2B, G2C, D2C, C2C, C2B (OR semantics)
negate_business_modellist[str] | NoneNoneBusiness-model labels to exclude, same values as business_model
start_datestr | NoneNoneYYYY-MM-DD, or a range YYYY-MM-DD,YYYY-MM-DD
min_digital_footprintint | NoneNoneMinimum digital footprint score, 0-800. Server default 50 when omitted
max_digital_footprintint | NoneNoneMaximum digital footprint score, 0-800. Server default 800 when omitted
exclude_leadgenbool | NoneTrueExclude suspected lead-generation sites: profiles scoring 25 or below with no phone, email, or social presence

Social, language, redirects:

FieldTypeDefaultDescription
sociallist[str] | NoneNoneRequire social presence: facebook, instagram, linkedin, pinterest, threads, tiktok, twitter, x, yelp, youtube, googleplay, applestore, amazon, vk, bluesky, xing. twitter is an alias for x
negate_sociallist[str] | NoneNoneExclude companies with these social profiles, same values as social
languagelist[str] | NoneNoneSite language, 2-letter codes, validated locally against the language list. See Languages
negate_languagelist[str] | NoneNoneLanguages to exclude, same values as language
redirectbool | NoneFalseInclude domains that redirect elsewhere

Tech stack and subdomains:

FieldTypeDefaultDescription
tech_stacklist[str] | NoneNoneLimit to companies using these vendor domains, up to 20
negate_tech_stacklist[str] | NoneNoneExclude companies using these vendor domains, up to 20
subdomainlist[str] | NoneNoneLimit results to these subdomains, up to 20, each at least 3 characters
negate_subdomainlist[str] | NoneNoneExclude these subdomains, up to 20, each at least 3 characters

Saved queries. Create and manage the underlying queries with client.queries:

FieldTypeDefaultDescription
inclusion_query_idlist[str] | NoneNoneLimit results to domains from these saved query results. Requires the STARTER plan
exclusion_query_idlist[str] | NoneNoneExclude domains from these saved query results

Result controls:

FieldTypeDefaultDescription
max_recordsint | None100Records to return, 5-10,000. Values outside the range are rejected locally
offsetint | None0Records to skip, for pagination. 0 or greater
min_similarityint | None0Minimum similarity score, 0-99
consensusint | None1Number of top results forming the consensus search vector, 1-20. Higher values reduce specificity
variancestr | None'UNRESTRICTED'Result diversity: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED
include_search_domainsbool | NoneFalseInclude the input domain values in the results

AI features:

FieldTypeDefaultDescription
enhancedbool | NoneFalseAI-powered result enhancement for relevance
retrievalbool | NoneFalseRetrieve page data using the Extract API
auto_icp_textbool | NoneFalseGenerate ICP text automatically from the provided domain values
auto_phrase_matchbool | NoneFalseGenerate phrase matches automatically from the ICP text

Returns list[Company].

from discolike.requests import DiscoverParams
results = client.discover(DiscoverParams(domain=["stripe.com"], country=["US"], max_records=5))
for company in results:
print(company.domain, company.similarity, company.name, company.employees)
# speedysaas.com 90.0 Speedy SaaS 1-10
# bluesnap.com 90.0 BlueSnap 201-500
# zylopay.net 90.0 Zylopay 11-50
client.count(params: CountParams) -> Count

CountParams is imported from discolike.requests.

CountParams carries a strict subset of DiscoverParams’s fields, with the same types, defaults, constraints, and meanings:

phrase_match, negate_phrase_match, subdomain, negate_subdomain, tech_stack, negate_tech_stack, category, negate_category, min_digital_footprint, max_digital_footprint, state, negate_state, country, negate_country, start_date, redirect, social, negate_social, language, negate_language, employee_range, revenue_range, business_model, negate_business_model, exclude_leadgen.

That list is the complete set of fields CountParams declares: it has no lookalike, paging, saved-query, or AI fields. Passing any other DiscoverParams field (domain, exclude_domain, icp_text, icp_prompt, min_similarity, consensus, variance, retrieval, enhanced, include_search_domains, auto_icp_text, auto_phrase_match, max_records, offset, exclusion_query_id, inclusion_query_id) does not fail locally; it reaches the API as an unrecognized parameter and is ignored.

Returns Count.

from discolike.requests import CountParams
total = client.count(CountParams(category=["SOFTWARE"], country=["US"], employee_range="51,200"))
print(total.count)
# 1651

Finds work email addresses from a name and a domain. Every call is asynchronous server-side: submitting returns a handle, and you poll that handle for the result. Email uses its own handle types, EmailJob and EmailBatch, rather than the Job class the rest of the SDK returns.

client.email.find(request: FindEmailRequest) -> EmailJob

FindEmailRequest is imported from discolike.requests.

FieldTypeDefaultDescription
first_namestrrequiredFirst name of the person.
last_namestrrequiredLast name of the person.
domainstrrequiredCompany domain to search, e.g. acme.com.
known_patternstr | NoneNoneKnown email local-part pattern for this domain, e.g. first.last. Pass it when you already know how a domain builds addresses and want the finder to try that shape first.

POST /email/find. Returns an EmailJob carrying job_id and kind (always "find"). Its wait() returns an EnumerationOutput rather than the EmailJobResult wrapper: the address is at output.result.email, and output.status is the enumeration outcome, not a job lifecycle status.

from discolike.requests import FindEmailRequest
job = client.email.find(FindEmailRequest(first_name="Patrick", last_name="Collison", domain="stripe.com"))
output = job.wait(timeout=120.0, poll_interval=3.0)
print(output.status, output.result.email, output.result.pattern)
# catch_all_pattern patrick.collison@stripe.com first.last
client.email.find_batch(request: FindEmailBatchRequest) -> EmailBatch

FindEmailBatchRequest is imported from discolike.requests.

FieldTypeDefaultDescription
requestslist[FindEmailRequest]requiredPeople to find, one FindEmailRequest each. 1-500 per batch; longer lists are rejected locally, so chunk them yourself.

POST /email/find/batch. Returns an EmailBatch with kind="find".

from discolike.requests import FindEmailBatchRequest, FindEmailRequest
batch = client.email.find_batch(FindEmailBatchRequest(requests=[
FindEmailRequest(first_name="Ada", last_name="Lovelace", domain="example.com"),
FindEmailRequest(first_name="Alan", last_name="Turing", domain="example.com"),
]))
results = batch.results(timeout=300.0, poll_interval=5.0)
print(results.total, results.completed, results.failed)
client.email.job(job_id: str, *, kind: Literal["find", "verify"] = "find") -> EmailJob
ParameterTypeDescription
job_idstrJob ID to rehydrate. Positional, not keyword
kindLiteral["find", "verify"]How the result payload is parsed. Defaults to "find"

Wraps an ID you already have so you can poll it later, useful when you submit in one process and collect in another. Makes no request. Pass kind="verify" to rehydrate a verify job; the default rehydrates find jobs. Returns an EmailJob.

job = client.email.job("ee58242e-2d7a-49cb-abae-5ed139aadbb6")
current = job.status()
print(current.status, current.result.result.email)
# completed patrick.collison@stripe.com
client.email.batch(batch_id: str, *, kind: Literal["find", "verify"]) -> EmailBatch
ParameterTypeDescription
batch_idstrBatch ID to rehydrate. Positional, not keyword
kindLiteral["find", "verify"]How each result payload is parsed. Required, no default

Makes no request. kind selects the decoding: "find" decodes results as EnumerationOutput, "verify" as ValidationOutput. Pass the kind that matches the batch you are polling. When the server reports each job’s own kind in the results (newer API versions), that takes precedence over the value passed here, so a handle rehydrated with the wrong kind still decodes correctly. Returns an EmailBatch.

Verify batches are created in the DiscoLike app or through the REST API, then re-attached here by ID:

results = client.email.batch("existing-verify-batch-id", kind="verify").results()
for item in results.results:
print(item.status, item.result.email if item.result else None)

append(), segment(), segment_file(), and validate_icp() are convenience forwarders on the client itself rather than on a resource namespace. Each takes a list of domains, from a file, a saved query, or a Python list. Unknown fields on the request model pass through to the wire; an invalid value raises pydantic.ValidationError when the model is constructed, before any request is made.

Defaults in the tables below are the model’s declared defaults, mirroring the server’s. A field you never set is not sent at all.

client.append(params: AppendParams, *, file: pathlib.Path | str | BinaryIO | None = None) -> list[AppendResult] | bytes

AppendParams is imported from discolike.requests.

FieldTypeDefaultDescription
datasetlist[str]requiredDatasets to append, at least one of bizdata, redirects, domain_status, growth, vendors. See the table below
query_idlist[str] | NoneNoneSaved-query IDs to resolve into domains, unioned with any domains from file. Optional if file is given
domain_columnstr | None'domain'Column in file holding domains
csvbool | NoneFalseReturn CSV bytes instead of parsed JSON rows
filepathlib.Path | str | BinaryIO | NoneNoneKeyword-only. CSV or Excel file of domains, uploaded as multipart. Optional if query_id is given

Synchronous: no job, no polling. POST /append. Passing neither file nor query_id raises ValueError: one of file or query_id is required before any request is made.

Datasets:

ValueWhat it appends
bizdataFull BizData firmographic profile: name, status, score, description, address, keywords, industry_groups, and the rest
domain_statusstatus_code, status_reason, record_date
redirectsredirect_sources (capped at 1,000 domains), redirect_count
growthQuarterly score_* and subdomains_* fields, score_growth_3m, subdomain_growth_3m
vendorsvendors: vendor and technology domains the company uses, capped at 100

Billing differs by dataset. bizdata bills net-new domains only; anything retrieved in the last 90 days is cached and free. The other four bill one record per row that returns data, with no cache, so re-running the same file bills them again. See the Append API for the full rules.

The return shape is decided by the response’s Content-Type header: a header containing application/json yields list[AppendResult], and anything else yields bytes, the raw response body. csv=True is what triggers the second case.

import io
from discolike.requests import AppendParams
rows = client.append(AppendParams(dataset=["bizdata"]), file=io.BytesIO(b"domain\nstripe.com\n"))
for row in rows:
print(row.domain, row.name, row.employees, row.revenue_range)
# stripe.com Stripe 10001+ >1B
client.segment(params: SegmentParams) -> Job

SegmentParams is imported from discolike.requests.

FieldTypeDefaultDescription
domainsstr | None''Comma-separated domains to segment
query_idlist[str] | NoneNoneSaved-query IDs whose resolved domains are segmented
max_segmentsint | NoneNoneMaximum number of segments to create, 2-20. Chosen automatically when omitted

Clusters domains into groups and returns a Job whose task_family is "segment". GET /segment: domains is sent as a single comma-separated string and query_id as one repeated query_id= parameter per ID. To segment domains from a file, use segment_file.

Passing neither domains nor query_id (or an empty domains) raises ValueError: one of domains or query_id is required client-side, before any request.

Results come back on status.results as BizData profiles carrying three extra fields:

FieldTypeDescription
segment_idIntegerAssigned segment. -1 means unclustered
segment_descriptionStringGenerated description of the segment. UNCLUSTERED for segment -1
probabilityFloatConfidence of the segment assignment

Only active businesses are segmented; closed or unindexed input domains are omitted from the results.

from discolike.requests import SegmentParams
job = client.segment(SegmentParams(domains="stripe.com,adyen.com,gusto.com", max_segments=5))
for row in job.wait().results:
print(row["domain"], row["segment_id"], row["segment_description"])
client.segment_file(params: SegmentFileParams, *, file: pathlib.Path | str | BinaryIO) -> Job

SegmentFileParams is imported from discolike.requests.

FieldTypeDefaultDescription
domain_columnstr | None'domain'Column in file holding domains
max_segmentsint | NoneNoneMaximum number of segments to create, 2-100. Chosen automatically when omitted
query_idlist[str] | NoneNoneSaved-query IDs whose resolved domains are segmented alongside the file’s
filepathlib.Path | str | BinaryIOrequiredKeyword-only. CSV or Excel file of domains, uploaded as multipart

Multipart POST /segment. Returns a Job whose task_family is "segment"; the job and its results behave exactly as documented under segment.

from pathlib import Path
from discolike.requests import SegmentFileParams
job = client.segment_file(SegmentFileParams(domain_column="website", max_segments=10), file=Path("accounts.csv"))
for row in job.wait().results:
print(row["domain"], row["segment_id"], row["segment_description"])
client.validate_icp(request: ValidateIcpRequest) -> Job

ValidateIcpRequest is imported from discolike.requests.

FieldTypeDefaultDescription
icp_textstrrequiredYour ICP description
domainslist[str]requiredDomains to validate, 1-10,000 items
context_modeLiteral['website', 'profile', 'domain'] | None'website'What the model sees per domain: website (profile plus homepage), profile (firmographics only), domain (name only)
integration_idstr | NoneNoneLLM provider integration UUID. Your default is used when omitted
web_searchbool | NoneFalseEnable web search enrichment
search_provider_idstr | NoneNoneSearch provider UUID for web search. Your default is used when omitted

POST /validate/icp. Scores each domain against an ICP description written in plain language. Returns a Job whose task_family is "discogen", not "segment"; validation runs on the DiscoGen pipeline and polls the DiscoGen status endpoint. It needs an LLM provider configured on your account.

Each domain comes back with three columns:

ColumnValuesDescription
Fityes, partial, noWhether the company matches the ICP
Confidencehigh, medium, lowHow confident the assessment is
ReasoningStringOne or two sentences explaining the verdict
from discolike.requests import ValidateIcpRequest
job = client.validate_icp(
ValidateIcpRequest(
icp_text="B2B SaaS companies providing HR and payroll software with 50-500 employees",
domains=["gusto.com", "rippling.com", "stripe.com"],
)
)
print(job.wait().results)

Manages bring-your-own-key LLM integrations, wrapping the LLM Providers endpoints. The integration_id values these return are what you pass as integration_id to discogen and validate_icp. For running the stack against your own infrastructure, see the self-hosting guide.

client.llm_providers.list() -> LLMProviderList

GET /llm-providers/config. Takes no arguments. Returns LLMProviderList.

for provider in client.llm_providers.list().providers:
print(provider.integration_id, provider.integration_name)
# 1f5e8466-fda4-4d4e-88eb-4b0886c37003 grok-4.5-latest
# 0ad8cd1a-70f8-48ee-ae53-42a0c8e94079 Perplexity fast search
client.llm_providers.get(*, integration_id: str) -> LLMProviderConfig
ParameterTypeDescription
integration_idstrIntegration UUID. Required

GET /llm-providers/config/{integration_id}. Returns LLMProviderConfig.

config = client.llm_providers.get(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003")
print(config.to_dict())
client.llm_providers.create(request: LLMProviderCreateRequest) -> LLMIntegrationResult

LLMProviderCreateRequest is imported from discolike.requests.

FieldTypeDefaultDescription
integration_namestrrequiredDisplay name for the integration
providerstrrequiredProvider slug, e.g. openai, anthropic, xai, gemini
api_keystrrequiredYour provider API key
model_namestrrequiredModel identifier, e.g. xai/grok-4.5-latest. Valid values come from client.discogen.models()
base_urlstr | NoneNoneOverride the provider endpoint, for gateways or self-hosted models

POST /llm-providers/config. Returns LLMIntegrationResult.

from discolike.requests import LLMProviderCreateRequest
result = client.llm_providers.create(
LLMProviderCreateRequest(
integration_name="My OpenAI", provider="openai", api_key="sk-...", model_name="gpt-5-mini"
)
)
print(result.integration_id, result.status, result.message)
client.llm_providers.update(request: LLMProviderUpdateRequest, *, integration_id: str) -> LLMIntegrationResult

LLMProviderUpdateRequest is imported from discolike.requests.

FieldTypeDefaultDescription
integration_namestrrequiredDisplay name
providerstrrequiredProvider slug
api_keystr | NonerequiredYour provider API key. Must be passed; None is sent as an explicit null and keeps the stored key
model_namestrrequiredModel identifier
base_urlstr | NoneNoneOverride the provider endpoint
integration_idstrrequiredKeyword-only. Integration UUID to replace

PUT /llm-providers/config/{integration_id}. A full replacement: integration_name, provider, and model_name are all required even when only one of them changes. Returns LLMIntegrationResult.

from discolike.requests import LLMProviderUpdateRequest
client.llm_providers.update(
LLMProviderUpdateRequest(
integration_name="Grok (production)",
provider="xai",
model_name="xai/grok-4.5-latest",
api_key="xai-...",
),
integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003",
)
client.llm_providers.delete(*, integration_id: str) -> None
ParameterTypeDescription
integration_idstrIntegration UUID. Required

DELETE /llm-providers/config/{integration_id}. Returns None.

client.llm_providers.delete(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003")
client.llm_providers.set_default(*, integration_id: str) -> LLMIntegrationResult
ParameterTypeDescription
integration_idstrIntegration UUID. Required

POST /llm-providers/config/{integration_id}/set-default. Sets the integration used whenever you omit integration_id on a DiscoGen or ICP validation call. Returns LLMIntegrationResult.

result = client.llm_providers.set_default(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003")
print(result.message)
client.llm_providers.test_connection(request: LLMProviderCreateRequest) -> LLMIntegrationResult

Takes the same LLMProviderCreateRequest as create, imported from discolike.requests; see that table for the fields.

POST /llm-providers/test-connection. Validates credentials without saving an integration, so it takes no integration_id. Returns LLMIntegrationResult.

from discolike.requests import LLMProviderCreateRequest
result = client.llm_providers.test_connection(
LLMProviderCreateRequest(
integration_name="probe", provider="openai", api_key="sk-...", model_name="gpt-5-mini"
)
)
print(result.status, result.message)

Resolves company names to domains and returns ranked candidates with a confidence score. Matches below a match_confidence of 50 are never returned; min_match_confidence raises that floor.

client.match.company(params: MatchCompanyParams) -> MatchResponse

MatchCompanyParams is imported from discolike.requests.

FieldTypeDefaultDescription
namestrrequiredCompany name to match
phonestr | NoneNonePhone number to disambiguate the match, E.164 or local format
citystr | NoneNoneCity to disambiguate the match
statestr | NoneNoneState or region to disambiguate the match
countrystr | NoneNoneISO-3166-1 alpha-2 country code to disambiguate the match
zip_codestr | NoneNoneZIP or postal code to disambiguate the match
strictbool | NoneFalseStrict matching, no filter relaxation
local_modebool | NoneFalsePreserve location filters during relaxation
min_match_confidenceint | None50Minimum match_confidence a match must have, 50-100

GET /match. Returns MatchResponse. Because matches defaults to an empty list rather than None, you can iterate it without a guard.

from discolike.requests import MatchCompanyParams
response = client.match.company(MatchCompanyParams(name="Stripe", country="US", min_match_confidence=80))
for match in response.matches:
print(match.domain, match.name, match.match_confidence)
# stripe.com Stripe 100.0
client.match.bulk(params: MatchBulkParams, *, file: pathlib.Path | str | BinaryIO) -> Job

MatchBulkParams is imported from discolike.requests.

FieldTypeDefaultDescription
name_columnstrrequiredColumn holding company names
phone_columnstr | NoneNoneColumn holding phone numbers
city_columnstr | NoneNoneColumn holding city names
state_columnstr | NoneNoneColumn holding state codes
country_columnstr | NoneNoneColumn holding country codes
zip_code_columnstr | NoneNoneColumn holding zip codes
strictbool | NoneFalseStrict matching, no filter relaxation
local_modebool | NoneFalsePreserve location filters during relaxation
min_match_confidenceint | None50Minimum match_confidence a match must have, 50-100
filepathlib.Path | str | BinaryIOrequiredKeyword-only. CSV or Excel file of company names

POST /bulkmatch. Uploads the file, starts a server-side job, and returns a Job whose task_family is "bulkmatch" immediately, without waiting for results. Results arrive on status.results, with the original input columns prefixed input: and a match_confidence on each row.

file accepts three shapes:

You passUpload filenameWho closes the handle
pathlib.PathThe path’s basenameThe SDK opens and closes it for you
str pathThe path’s basenameThe SDK opens and closes it for you
Open binary handleThe handle’s .name basename, or upload.csv if it has noneYou do; the SDK leaves it open

An in-memory io.BytesIO works as the third shape and is sent as upload.csv. append() and segment_file() handle file the same way.

from pathlib import Path
from discolike.requests import MatchBulkParams
job = client.match.bulk(MatchBulkParams(name_column="company", min_match_confidence=80), file=Path("companies.csv"))
status = job.wait(timeout=1800, poll_interval=10)
for row in status.results:
print(row)

Manages saved queries: the named, reusable result sets you feed back into discovery, append, and segment as query_id. Wraps the Queries endpoints.

Defaults in the tables below are the model’s declared defaults and mirror the server’s. A field you never set is not sent at all.

client.queries.list(params: QueriesListParams) -> SavedQueries

QueriesListParams is imported from discolike.requests.

FieldTypeDefaultDescription
max_recordsint | None100Maximum records to return, 1-1000
offsetint | None0Records to skip, for pagination. 0 or more
actionstr | NoneNoneFilter by action type, e.g. discover, exclusion. Matches partially, so discover also returns thin_discover rows
tagslist[str] | NoneNoneFilter by tags; matches queries carrying any of the given tags

GET /queries/saved. Returns saved queries that have associated domains, as SavedQueries. Its count is the total number of matching queries on the server, not the number returned in results. Page through by incrementing offset.

from discolike.requests import QueriesListParams
saved = client.queries.list(QueriesListParams(max_records=3))
print(saved.count)
for query in saved.results:
print(query.query_name, query.action, query.domain_count)
# 1916
# ContaGen: Owner, founder, executive, marketing and ecommerce contacts from selected companies thin_contagen 3
# USA law firms in United States discover 100
client.queries.create_exclusion_list(request: CreateExclusionListRequest) -> QueryResult

CreateExclusionListRequest is imported from discolike.requests.

FieldTypeDefaultDescription
query_namestrrequiredName for the saved list, 1-255 characters
domainslist[str] | NoneNoneDomains to exclude
persona_idslist[int] | NoneNonePersona IDs to exclude
tagslist[str] | NoneNoneTags to attach to the saved query. Up to 20 items, each 2-50 characters of letters, digits, hyphens, or underscores

POST /queries/exclusion-list. Saves a set of domains or personas as a named exclusion list you can later pass as query_id to suppress those records. Returns QueryResult.

from discolike.requests import CreateExclusionListRequest
result = client.queries.create_exclusion_list(
CreateExclusionListRequest(
query_name="Existing customers", domains=["stripe.com", "shopify.com"], tags=["q3-outbound"]
)
)
print(result.query_id, result.domain_count)
client.queries.save_results(request: SaveResultsRequest) -> QueryResult

SaveResultsRequest is imported from discolike.requests.

FieldTypeDefaultDescription
query_namestrrequiredName for the saved query, 1-255 characters
actionstrrequiredAction type to record against the query. One of discover, segment, contacts, append, match
datalist[dict[str, Any]]requiredThe rows to save. At least one row
query_paramsdict[str, Any] | NoneNoneArbitrary parameters to store alongside the query, for provenance
domain_columnstr | None'domain'Which key in each row holds the domain. Up to 128 characters
tagslist[str] | NoneNoneTags to attach to the saved query. Up to 20 items

POST /queries/save-results. Saves rows you already have (from your own pipeline, a CSV, or a previous SDK call) so they can be referenced by query_id downstream. Returns QueryResult.

from discolike.requests import SaveResultsRequest
result = client.queries.save_results(
SaveResultsRequest(
query_name="Inbound trial signups",
action="discover",
data=[{"domain": "stripe.com"}, {"domain": "shopify.com"}],
domain_column="domain",
tags=["inbound"],
)
)
print(result.query_id, result.row_count)
client.queries.update(request: UpdateQueryRequest, *, query_id: str) -> QueryResult

UpdateQueryRequest is imported from discolike.requests.

FieldTypeDefaultDescription
query_namestr | NoneNoneNew display name, up to 255 characters
tagslist[str] | NoneNoneReplacement tag list, up to 20 items
query_idstrrequiredKeyword-only. Query UUID to update

PATCH /queries/{query_id}. Fields you leave unset are not sent and are left untouched server-side. Returns QueryResult.

from discolike.requests import UpdateQueryRequest
client.queries.update(
UpdateQueryRequest(query_name="Q3 target accounts"), query_id="43a6cf1c-859a-4a66-b144-65a38405cbfd"
)
client.queries.delete(*, query_id: str) -> None
ParameterTypeDescription
query_idstrQuery UUID to delete. Required

DELETE /queries/{query_id}. Returns None. A missing query_id raises NotFoundError.

client.queries.delete(query_id="43a6cf1c-859a-4a66-b144-65a38405cbfd")

Manages bring-your-own-key web search integrations, wrapping the Search Providers endpoints. The integration_id values these return are what you pass as search_provider_id to discogen and validate_icp.

client.search_providers.list() -> SearchProviderList

GET /search-providers. Takes no arguments. Returns SearchProviderList. api_key comes back masked as *****, and is_default tells you which integration is used when you omit search_provider_id.

for provider in client.search_providers.list().providers:
print(provider.integration_id, provider.integration_name)
# fd467f7f-11c1-4475-923a-f2c3341e41bb Serper
# 9d52cfb2-88e2-4a36-befb-a2d4d89ee0a1 Linkup Search
client.search_providers.create(request: SearchProviderRequest) -> SearchProviderConfig

SearchProviderRequest is imported from discolike.requests. update() takes the same model.

FieldTypeDefaultDescription
integration_namestrrequiredDisplay name for the integration
providerstrrequiredProvider slug, e.g. serper, linkup, tavily
search_modelstrrequiredModel identifier, e.g. serper/search. Get valid values from models()
api_keystr | NoneNoneYour provider API key. Not sent when unset
base_urlstr | NoneNoneOverride the provider endpoint, for self-hosted backends such as SearXNG

POST /search-providers. Returns SearchProviderConfig.

from discolike.requests import SearchProviderRequest
config = client.search_providers.create(
SearchProviderRequest(
integration_name="My Serper", provider="serper", search_model="serper/search", api_key="..."
)
)
print(config.integration_id)
client.search_providers.update(request: SearchProviderRequest, *, integration_id: str) -> SearchProviderConfig

SearchProviderRequest is imported from discolike.requests; it is the same model create() takes.

FieldTypeDefaultDescription
integration_namestrrequiredDisplay name
providerstrrequiredProvider slug
search_modelstrrequiredModel identifier
api_keystr | NoneNoneYour provider API key. Leave unset, or pass None, to keep the stored key
base_urlstr | NoneNoneOverride the provider endpoint
integration_idstrrequiredKeyword-only. Integration UUID to replace

PUT /search-providers/{integration_id}. A full replacement: integration_name, provider, and search_model are all required even when only one of them changes, so read the current values with list() first and pass them back. Returns SearchProviderConfig.

from discolike.requests import SearchProviderRequest
client.search_providers.update(
SearchProviderRequest(
integration_name="Serper (production)", provider="serper", search_model="serper/search"
),
integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb",
)
client.search_providers.delete(*, integration_id: str) -> None
ParameterTypeDescription
integration_idstrIntegration UUID. Required

DELETE /search-providers/{integration_id}. Returns None.

client.search_providers.delete(integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb")
client.search_providers.set_default(*, integration_id: str) -> SearchProviderResult
ParameterTypeDescription
integration_idstrIntegration UUID. Required

PUT /search-providers/{integration_id}/default. The default integration is used whenever you omit search_provider_id on a DiscoGen call. Returns SearchProviderResult.

result = client.search_providers.set_default(
integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb"
)
print(result.message, result.integration_id)
client.search_providers.clear_default(*, integration_id: str) -> SearchProviderResult
ParameterTypeDescription
integration_idstrIntegration UUID. Required

DELETE /search-providers/{integration_id}/default. Returns SearchProviderResult.

client.search_providers.clear_default(
integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb"
)
client.search_providers.models() -> SearchModels

GET /search-providers/models. Lists the search models you can configure, grouped by provider, with per-query cost. Takes no arguments. Returns SearchModels.

models = client.search_providers.models()
print(list(models.models))
print([m.model_dump() for m in models.models["apiserpent"]][:2])
# ['apiserpent', 'dataforseo', 'duckduckgo', 'exa_ai', 'firecrawl', 'linkup', 'parallel_ai', 'perplexity', 'searxng', 'serper', 'tavily', 'tinyfish', 'you_com']
# [{'name': 'apiserpent/search/google', 'cost_per_query': 0.0006}, {'name': 'apiserpent/search/bing', 'cost_per_query': 0.0006}]

Account creation for a person who has no DiscoLike account yet. Wraps the Signup API. A module-level function, not a client method: it needs no credential and no Discolike instance, and it takes keyword arguments rather than a request model.

signup(*, email: str, first_name: str, last_name: str, agent: str | None = None, base_url: str = "https://api.discolike.com/v1", timeout: float = 60.0, http_client: Client | None = None, allow_new_email: bool = False) -> SignupResult

POST /public/signup. Creates a DiscoLike user and organization for email. No credential is returned: the person gets a confirmation email and logs in at app.discolike.com. Returns SignupResult.

ArgumentTypeDefaultDescription
emailstrrequiredThe person’s work email. Free-mail and disposable domains are rejected
first_namestrrequired1-40 characters after NFC normalization and trimming. Must contain a letter and no angle brackets or control characters
last_namestrrequiredSame rules as first_name
agentstr | NoneNoneAgent or framework name recorded with the signup. Defaults to discolike-python/<version>
base_urlstrhttps://api.discolike.com/v1API base URL
timeoutfloat60.0Request timeout in seconds
http_clientClient | NoneNoneReuse an existing HTTP client instead of opening one. Not closed for you
allow_new_emailboolFalseSign up an email different from the one this machine signed up before

Names are validated locally before any request, raising ValidationError on failure. A successful signup records the email on the machine; a later signup() with a different email raises DiscolikeError unless allow_new_email=True, so an agent cannot quietly create a second account. 409 means the account already exists.

from discolike import signup
result = signup(email="jane@acme.com", first_name="Jane", last_name="Doe", agent="my-agent")
print(result.next_step)
A confirmation email was sent to jane@acme.com. Log in at https://app.discolike.com. Google or Microsoft sign-in with this email also works, no password needed.
await async_signup(*, email: str, first_name: str, last_name: str, agent: str | None = None, base_url: str = "https://api.discolike.com/v1", timeout: float = 60.0, http_client: AsyncClient | None = None, allow_new_email: bool = False) -> SignupResult

Identical to signup, awaited. http_client takes an async client.

Models, credentials, job handles, and exceptions. Response models subclass DiscolikeModel; request models subclass DiscolikeRequest.

DiscolikeModel is the base for every model in the SDK and is configured with extra="allow", so response keys a model does not declare are kept rather than dropped.

AccessResult
model.to_dict()The full response as a plain JSON-mode dict, extras included. Use this by default.
model.model_extraA dict of just the extra fields
model.<name>Attribute access, which works for extras too

Prefixed names such as redirects:redirect_count are not valid Python identifiers, so to_dict() or model_dump() is the route to those.

client.contacts.count() and client.contacts.discover() return DiscolikeModel itself, with no declared fields at all; the whole response body is in the extras.

DiscolikeRequest is the base for every model in discolike.requests, the request models the methods take. It is configured with extra="allow" and populate_by_name=True. Its to_wire() method produces the query string or JSON body the client sends, and follows four rules:

  • Only fields you set are sent. A field left at its default is omitted entirely, so the server’s default governs.
  • A field you set to None explicitly is kept in the output. JSON-body routes send it as null; query-parameter routes drop it before the request, since a query string has no null.
  • Fields the model does not declare pass through unchanged.
  • A field declared with an alias is sent under its wire name, and accepts either the Python name or the alias on construction.
from discolike.requests import MatchCompanyParams
print(MatchCompanyParams(name="Acme", city=None).to_wire())
print(MatchCompanyParams.model_validate({"name": "Acme", "bogus": 1}).to_wire())
# {'name': 'Acme', 'city': None}
# {'name': 'Acme', 'bogus': 1}

Field constraints are validated on construction, before any request is made; see Exceptions. DiscolikeRequest itself is importable from the package root, the models from discolike.requests:

from discolike import DiscolikeRequest
from discolike.requests import MatchCompanyParams

The full set of models, by namespace:

NamespaceModelMethod
rootDiscoverParamsdiscover()
rootCountParamscount()
rootValidateIcpRequestvalidate_icp()
rootAppendParamsappend()
rootSegmentParamssegment()
rootSegmentFileParamssegment_file()
companiesCompaniesDataParamscompanies.data()
companiesCompaniesScoreParamscompanies.score()
companiesCompaniesGrowthParamscompanies.growth()
companiesCompaniesExtractParamscompanies.extract()
companiesCompaniesRedirectsParamscompanies.redirects()
companiesCompaniesVendorsParamscompanies.vendors()
companiesCompaniesSubsidiariesParamscompanies.subsidiaries()
companiesCompaniesPublicLinksParamscompanies.public_links()
matchMatchCompanyParamsmatch.company()
matchMatchBulkParamsmatch.bulk()
contactsContactsSearchParamscontacts.search()
contactsContactsCountParamscontacts.count()
contactsContactsLookupParamscontacts.lookup()
contactsContactsMatchParamscontacts.match()
contactsBulkContactMatchRequestcontacts.bulk_match()
contactsBulkContactMatchQueryItemOne entry of BulkContactMatchRequest.queries
contactsContactFilterscontacts.discover()
contactsContactGenerateRequestcontacts.generate()
discogenDiscoGenProcessRequestdiscogen.process()
discogenDiscoGenPersonaProcessRequestdiscogen.process_personas()
emailFindEmailRequestemail.find()
emailFindEmailBatchRequestemail.find_batch()
queriesQueriesListParamsqueries.list()
queriesCreateExclusionListRequestqueries.create_exclusion_list()
queriesSaveResultsRequestqueries.save_results()
queriesUpdateQueryRequestqueries.update()
search_providersSearchProviderRequestsearch_providers.create() and update()
llm_providersLLMProviderCreateRequestllm_providers.create() and test_connection()
llm_providersLLMProviderUpdateRequestllm_providers.update()

The base profile model. BizData (from companies.data) is CompanyProfile with no added fields; Company and MatchResult each add one.

FieldTypeDescription
domainstr | NoneNormalized domain, the unique identifier
namestr | NoneCompany name from certificate or website
statusCompanyStatus | NoneOperating status with confidence
scoreint | NoneDigital footprint score (1-800)
start_datestr | NoneFirst certificate date, an estimate of company start
end_datestr | NoneLast certificate date if closed, None if active
addressCompanyAddress | NoneHQ address
phoneslist[str] | NonePhone numbers from the website
public_emailslist[str] | NoneContact emails from the website
domain_associationslist[str]Associated domains. Defaults to []
social_urlslist[str] | NoneSocial profile URLs
redirect_domainstr | NoneFinal domain if the site redirects
descriptionstr | NoneCompany description from the website
keywordsdict[str, float]Keyword to confidence score. Defaults to {}
industry_groupsdict[str, float]Industry classification to score. Defaults to {}
employeesstr | NoneEmployee bucket: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+
revenue_rangestr | NoneRevenue bucket: <1M, 1-10M, 10-100M, 100M-1B, >1B, or N/A
business_modeldict[str, float]Business-model label to confidence. Defaults to {}
update_datestr | NoneLast record update
mx_providerstr | NoneMail host domain, no_mx if the domain has no mail server, None if unresolved
linkupNoneAnnotated as the type None and always null. Present for wire compatibility

CompanyStatus: status: str | None, confidence: float | None.

CompanyAddress: street, city, state, zip, country, all str | None.

vendors arrives on company results as an extra field rather than a declared one.

Returned by discover(). CompanyProfile plus one field:

FieldTypeDescription
similarityfloat | NoneSimilarity between the result and your query (0-100)

Returned by count().

FieldTypeDescription
countint | NoneNumber of companies matching the filters

Returned by companies.score().

FieldTypeDescription
domainstr | NoneNormalized domain
scoreint | NoneComposite score, 0-800
parametersScoreParameters | NoneScore components
first_eventstr | NoneDate of the first observed certificate

ScoreParameters: base_score: float | None, recency_multiplier: float | None, growth_boost: float | None, lookback_360: int | None, lookback_720: int | None. It also carries expiration_penalty as an extra.

Returned by companies.growth().

FieldTypeDescription
domainstr | NoneNormalized domain
score_growth_3mfloat | NoneScore growth rate over the last three months
subdomain_growth_3mfloat | NoneSubdomain growth rate over the last three months

The API also returns one score_YYYYQX and one subdomains_YYYYQX key per quarter, as extras.

Returned by companies.extract().

FieldTypeDescription
textstr | NoneExtracted page text
languagestr | NoneDetected language code

Returned by companies.redirects().

FieldTypeDescription
source_domainstr | NoneNormalized source domain
source_fqdnstr | NoneFull source URL
linked_domainstr | NoneNormalized destination domain
linked_fqdnstr | NoneFull destination URL
record_datestr | NoneDate the record was compiled

Returned by companies.vendors().

FieldTypeDescription
client_domainstr | NoneNormalized client domain
client_fqdnstr | NoneClient’s full URL
vendor_domainstr | NoneNormalized vendor domain
vendor_fqdnstr | NoneVendor’s full URL
record_datestr | NoneDate the record was compiled

Returned by companies.subsidiaries().

FieldTypeDescription
source_domainstr | NoneNormalized source domain
source_fqdnstr | NoneFull source URL
source_scoreint | NoneSource domain score (1-800)
linked_domainstr | NoneNormalized linked domain
linked_fqdnstr | NoneFull linked URL
linked_scoreint | NoneLinked domain score (1-800)
parent_domainstr | NoneDomain with the highest score
child_domainstr | NoneSubsidiary domain
record_datestr | NoneDate the record was compiled

Returned by companies.public_links().

FieldTypeDescription
domainstr | NoneNormalized query domain
linked_domainstr | NoneRelated domain found
link_valueslist[str]Shared contact values establishing the link. Defaults to []
record_datestr | NoneDate the record was compiled

Returned by contacts.search() and contacts.lookup().

FieldType
persona_idint | None
domainstr | None
namestr | None
titlestr | None
emailstr | None

The API returns considerably more per person: department, seniority, skills, phone, social_urls, connections, country, state, industry, employees, revenue_range, jobstart_date, company_name, summary, and others. Those are preserved as extras.

Returned by contacts.match().

FieldTypeDescription
queryContactMatchQuery | NoneThe query the server ran, echoed back
matcheslist[ContactMatchResult]Ranked candidates. Defaults to []

ContactMatchQuery: name, company_name, domain, person_country, all str | None.

ContactMatchResult: persona_id: int | None, name: str | None, title: str | None, domain: str | None, company_name: str | None, match_score: float | None.

Every field defaults to None or an empty list, so a sparse response never raises a validation error.

ContactsDiscoverResponse / ContactsByCompany

Section titled “ContactsDiscoverResponse / ContactsByCompany”

Returned by contacts.discover().

FieldTypeDescription
resultsdict[str, ContactsByCompany]Keyed by company domain. Defaults to {}
total_contactsint | NoneMatching contacts across all pages, not just this batch
total_domainsint | NoneUnique domains in this batch

ContactsByCompany extends CompanyProfile with all the firmographic fields, and adds contacts: list[Contact] (defaults to []), email_pattern, email_pattern_confidence, and email_pattern_guess (the inferred company email pattern, its 0–1 confidence, and a best-effort example address; each None when no pattern is available).

Returned by match.company().

FieldTypeDescription
queryMatchQuery | NoneThe query the API echoed back
matcheslist[MatchResult]Ranked candidates, empty list when nothing matched

MatchQuery carries name, country, state, city, zip, and phones, all str | None. The echoed values are zip and phones; the request parameters are zip_code and phone.

CompanyProfile plus one field:

FieldTypeDescription
match_confidencefloat | NoneMatch confidence, 0-100

Returned by append() when the response is JSON.

FieldTypeDescription
domainstr | NoneThe domain the row is for

Every appended column arrives as an extra, because the column set changes with the datasets you requested and the naming scheme the API picks.

row = rows[0]
print(row.domain)
print(row.model_dump(exclude_none=True))
{
"domain": "stripe.com",
"input:domain": "stripe.com",
"name": "Stripe",
"status": { "status": "active", "confidence": 0.89 },
"score": 701,
"start_date": "2011-01-04",
"address": {
"street": "354 Oyster Point Blvd",
"city": "South San Francisco",
"state": "CA",
"zip": "94080",
"country": "US"
},
"business_model": { "B2B": 0.97 },
"employees": "10001+",
"revenue_range": ">1B",
"mx_provider": "google.com",
"update_date": "2026-07-22"
}

description, keywords, industry_groups, social_urls, public_emails, phones, and domain_associations are omitted above for length; they arrive as extras alongside the rest.

What email.find() produces.

FieldType
first_name, last_name, domain, status, mx_host, provider, errorstr | None
resultEnumerationMatch | None
is_catch_allbool | None
attempts, duration_msint | None

EnumerationMatch, the address itself:

FieldType
email, patternstr | None
tier, smtp_codeint | None
validbool | None

For the meaning of each status value, see Email Find.

What a "verify" batch produces.

FieldType
email, status, mx_host, provider, error, reasonstr | None
is_deliverable, is_catch_allbool | None
smtp_code, attempts, duration_msint | None

The per-job wrapper.

FieldType
job_idstr | None
statusstr | None
resultEnumerationOutput | ValidationOutput | None
errorstr | None

The union member is picked from each result’s server-reported kind when present (newer API versions), falling back to the handle’s kind, which is why the kind you pass to email.batch() should still match the batch.

EmailBatchResults: batch_id: str | None, total: int | None, completed: int | None, failed: int | None, results: list[EmailJobResult] (defaults to []).

Returned by discogen.models().

FieldTypeDescription
modelsdict[str, list[DiscogenModelInfo]]Model lists keyed by provider name. Defaults to {}

DiscogenModelInfo: name: str | None, supports_web_search: bool | None.

Returned by queries.list().

FieldTypeDescription
resultslist[SavedQuery]The page of saved queries. Defaults to []
countint | NoneTotal matching queries on the server
FieldTypeDescription
query_idstr | NoneUUID to pass downstream as query_id
query_namestr | NoneDisplay name
actionstr | NoneAction type that produced the query
user_namestr | NoneWho created it
mtimestr | NoneLast modified timestamp, ISO 8601
domainslist[str] | NoneDomains attached to the query
domain_countint | NoneNumber of domains
persona_id_countint | NoneNumber of personas
tagslist[str]Attached tags. Defaults to []

The API also returns query_params on each row (the stored parameters that produced the query) as an extra.

Returned by create_exclusion_list, save_results, and update.

FieldTypeDescription
query_idstr | NoneUUID of the created or updated query
query_namestr | NoneDisplay name
actionstr | NoneAction type recorded against the query
domain_countint | NoneNumber of domains
persona_id_countint | NoneNumber of personas
row_countint | NoneNumber of rows saved
tagslist[str] | NoneAttached tags
FieldTypeDescription
providerslist[SearchProviderConfig]Configured search integrations. Defaults to []
FieldTypeDescription
integration_idstr | NoneUUID to pass as search_provider_id
integration_namestr | NoneDisplay name

The API additionally returns provider, search_model, api_key (masked), base_url, is_default, cost_per_query, and encrypted_api_key as extras:

{'integration_id': 'fd467f7f-11c1-4475-923a-f2c3341e41bb', 'integration_name': 'Serper',
'provider': 'serper', 'search_model': 'serper/search', 'encrypted_api_key': None,
'api_key': '*****', 'base_url': None, 'is_default': True, 'cost_per_query': 0.001}
FieldTypeDescription
messagestr | NoneServer message
integration_idstr | NoneAffected integration
FieldTypeDescription
modelsdict[str, list[SearchModelInfo]]Model lists keyed by provider. Defaults to {}

SearchModelInfo: name: str | None, cost_per_query: float | None (USD per search query).

FieldTypeDescription
providerslist[LLMProviderConfig]Configured LLM integrations. Defaults to []
mtimestr | NoneLast modified timestamp of the configuration set
FieldTypeDescription
integration_idstr | NoneUUID to pass as integration_id on DiscoGen calls
integration_namestr | NoneDisplay name

The API additionally returns provider, api_key (masked), model_name, base_url, supports_web_search, input_cost_per_token, output_cost_per_token, is_default, and model_deprecated as extras:

{'integration_id': '1f5e8466-fda4-4d4e-88eb-4b0886c37003', 'integration_name': 'grok-4.5-latest',
'provider': 'xai', 'api_key': '*****', 'model_name': 'xai/grok-4.5-latest', 'base_url': None,
'supports_web_search': True, 'input_cost_per_token': 2e-06, 'output_cost_per_token': 6e-06,
'is_default': True, 'model_deprecated': False}
FieldTypeDescription
messagestr | NoneServer message
integration_idstr | NoneAffected integration
statusstr | NoneResult status

Returned by account.usage(). It declares three fields, and the live API returns none of them:

FieldTypeDescription
requests_mtdint | NoneNone against the live API
records_mtdint | NoneNone against the live API
spend_mtdfloat | NoneNone against the live API

The month-to-date values arrive under different names, as extras:

Extra fieldTypeDescription
month_to_date_requestsintRequests made this month
month_to_date_recordsintRecords returned this month
month_to_date_spendfloatSpend this month, in USD
account_statusstrAccount state, e.g. active
max_spendstr | floatSpend cap, or "unlimited"
total_available_spendstr | floatRemaining spend, or "unlimited"
carryover_creditsfloatCredits carried over from the previous period
top_up_creditsfloatCredits added by top-up
usage_summarydictPer-month breakdown keyed by YYYY-MM. Each row carries access_id (returned masked by the API, e.g. ••••••••isco), description, requests, total_records, and monthly_spend
billing_eventslist[dict]Recent billable calls, newest first. Each entry has created_at, action, user_email, api_key_id, api_key_description, task_id, companies_billed, contacts_billed, and cost_usd
recent_companies_billedintCompanies billed in the recent window
recent_contacts_billedintContacts billed in the recent window
estimated_provider_spend_recentfloatEstimated BYOK provider spend in the recent window
data = client.account.usage().to_dict()
print(data["month_to_date_spend"], data["account_status"])
# 26.37 active

Returned by signup and async_signup.

FieldTypeDescription
statusstrcreated
emailstrNormalized email the account was created for
org_domainstrOrganization domain derived from the email
org_statusstrcreated when a new organization was made, joined when one already existed for the domain
next_stepstrInstruction to relay to the account owner

The two credential types the client’s auth argument accepts. Both are frozen dataclasses importable from the package root, and both are what the SDK reads out of the CLI config file; see Authentication for the resolution order and the file layout.

from discolike import ApiKeyCredential, Discolike, OAuthCredential
client = Discolike(auth=ApiKeyCredential(api_key="dk_..."))
client = Discolike(
auth=OAuthCredential(
access_token="eyJ...",
refresh_token="...",
expires_at=1756426800.0,
client_id="...",
token_endpoint="https://auth.discolike.com/oauth/2.1/token",
)
)
ClassFieldTypeDescription
ApiKeyCredentialapi_keystrSent as the X-discolike-key header on every request
OAuthCredentialaccess_tokenstrSent as Authorization: Bearer on every request
refresh_tokenstrExchanged at token_endpoint when the access token is about to expire or a request returns 401
expires_atfloatUnix timestamp the access token expires at. The client refreshes within 60 seconds of it
client_idstrThe OAuth client the tokens were issued to; sent with every refresh
token_endpointstrWhere refreshes are posted; the CLI fills it in from the authorization server’s metadata

OAuthCredential.expires_within(seconds) returns whether the access token expires within that many seconds from now. Instances are immutable: a refresh produces a new OAuthCredential inside the client and never mutates the one you passed in. Refresh, replay-on-401, and write-back behaviour are described under OAuth sessions.

The handle returned by the seven job-returning methods listed in Jobs and polling.

class Job:
task_family: str
task_id: str
def status(self) -> JobStatus: ...
def cancel(self) -> None: ...
def wait(
self,
*,
timeout: float = 900.0,
poll_interval: float = 5.0,
on_poll: Callable[[JobStatus], None] | None = None,
) -> JobStatus: ...
MemberTypeDescription
task_familystrOne of bulkmatch, contactmatch, discogen, segment. Determines the polling path
task_idstrServer-assigned task identifier. Store this to resume polling in another process
status()-> JobStatusOne GET /{task_family}/status/{task_id} request. Returns the current status without blocking
cancel()-> NoneDELETE /{task_family}/cancel/{task_id}. Returns as soon as the request is accepted; the task stops asynchronously
wait()-> JobStatusPolls status() until the task reaches a terminal status, then returns that status

wait() parameters:

ParameterTypeDefaultDescription
timeoutfloat900.0Seconds before JobTimeoutError is raised. Measured on a monotonic clock, so it is unaffected by system clock changes
poll_intervalfloat5.0Fixed seconds slept between fetches
on_pollCallable[[JobStatus], None] | NoneNoneCalled with each fetched status

wait() loops: fetch status, call on_poll if you supplied one, decide, then sleep poll_interval before the next fetch. The deadline is checked after each fetch, so wait() always makes at least one request, even with timeout=0.

  • On status == "failed", wait() raises JobFailedError. The message is str(status.result), falling back to "task failed"; exc.payload is the full status dict.
  • On status == "completed" or "cancelled", wait() returns the JobStatus. A cancelled task returns with empty results, so check status.status before using them.
  • On timeout, wait() raises JobTimeoutError. The task keeps running server-side. Call wait() again on the same handle to resume, or poll status() yourself later.

cancel() issues the delete and returns immediately, without waiting for the task to stop. A wait() already running in another thread keeps polling until it observes the terminal status. To cancel from a poll callback, raise out of on_poll:

def stop_if_slow(status):
if status.progress is not None and status.progress < 5:
job.cancel()
raise TimeoutError("job is not progressing")
job.wait(on_poll=stop_if_slow)

AsyncJob has the same attributes and the same three methods, with status(), cancel(), and wait() awaited and sleeps done through asyncio.sleep. on_poll stays a plain synchronous callable on AsyncJob; it is called, not awaited, so keep it non-blocking.

discogen.job() rehydrates a discogen handle from a task ID. For the bulkmatch, contactmatch, and segment families, construct a Job against the client’s transport:

from discolike import Job
job = Job(client._transport, task_family="segment", task_id=saved_task_id)
AttributeTypeDescription
statusstrRequired. Terminal values are completed, failed, cancelled. Anything else means still running
progressint | NonePercent complete, when the server reports it
resultsAnyThe list-shaped payload for a finished task
resultAnyThe scalar payload, and also where a failure message lands
warningslist[str]Defaults to an empty list
estimated_costfloat | NoneDiscoGen family only. Best-effort spend on your own provider keys
cost_metadatadict[str, dict[str, Any]] | NoneDiscoGen family only. One entry per provider/model with calls, search_calls, prompt_tokens, completion_tokens, est_cost_usd, plus a search_provider entry when a BYOS search provider ran

JobStatus allows extra fields, so anything the server adds is preserved.

Email finding uses its own handle types. Each carries its own identifier (job_id on EmailJob, batch_id on EmailBatch) plus a kind, polls its own endpoints, and has completed and failed as its terminal statuses. Neither offers cancellation.

HandlePoll methodReturns
EmailJobstatus(), then wait(*, timeout=900.0, poll_interval=5.0, on_poll=None)wait() returns the unwrapped output for the handle’s kind: EnumerationOutput for find jobs and ValidationOutput for verify jobs; status() returns EmailJobResult
EmailBatchresults(*, timeout=900.0, poll_interval=5.0, on_poll=None), which polls and returns in one callEmailBatchResults

Both take the same three keyword arguments:

ParameterTypeDefaultDescription
timeoutfloat900.0Seconds before JobTimeoutError is raised
poll_intervalfloat5.0Seconds slept between polls
on_pollCallable[[...], None] | NoneNoneCalled with each fetched result, on every poll including the last

The deadline is checked after each fetch, so both methods always make at least one request, even with timeout=0. A JobTimeoutError means the work is still running server-side. Call the method again to resume; nothing is lost.

EmailJob.wait() raises JobFailedError when status == "failed", with the message from current.error or "email {kind} job failed" and payload set to the full result dict, and raises JobFailedError("email {kind} job completed without a result") if the job reaches a terminal status but the decoded result is not the output type expected for the handle’s kind. Otherwise it returns that output: EnumerationOutput for find and ValidationOutput for verify.

EmailBatch.results() considers a batch done when len(results) >= total and every item’s status is completed or failed. It never raises JobFailedError; individual failures show up as items with status == "failed" and a populated error.

on_poll stays a synchronous callable on AsyncEmailJob and AsyncEmailBatch too; it is called directly, not awaited.

Every class below inherits from DiscolikeError, which itself inherits from Exception, so a single except DiscolikeError catches all of them. DiscolikeError.__init__ is (message, *, status_code=None, payload=None), so every exception carries .status_code and .payload. All of them are importable from the package root:

from discolike import DiscolikeError, RateLimitError, ValidationError
ExceptionBaseRaised whenAttributes
DiscolikeErrorExceptionAny 4xx status not mapped to a more specific classstatus_code: int | None, payload: Any
AuthenticationErrorDiscolikeErrorHTTP 401. Also raised at client construction when no credential resolves (status_code is None), and from any call whose OAuth token refresh the authorization server rejects, with the message OAuth session expired; run `discolike auth login` inherited
PlanAccessErrorDiscolikeErrorHTTP 402 or 403inherited
ValidationErrorDiscolikeErrorHTTP 400 or 422inherited
NotFoundErrorDiscolikeErrorHTTP 404inherited
ServerErrorDiscolikeErrorAny status ≥ 500, after retries are exhaustedinherited
RateLimitErrorDiscolikeErrorHTTP 429, after retries are exhaustedinherited, plus retry_after: float | None
APIConnectionErrorDiscolikeErrorA transport-level failure that is not retryable, or retries exhausted. status_code and payload are always Noneinherited
JobFailedErrorDiscolikeErrorA job reaches status == "failed", or an email find job completes without a resultinherited; payload is the job status dict
JobTimeoutErrorDiscolikeErrorThe client-side wait deadline elapses. The task keeps running server-sideinherited

retry_after is parsed from the Retry-After response header and is None when the header is absent or non-numeric.

The exception message is extracted from the response body:

BodyMessage
{"detail": "..."}that string
{"detail": [{"loc": [...], "msg": "..."}]}"loc.joined: msg" parts joined with "; "
Any other JSONjson.dumps(payload) truncated to 500 characters
Non-JSONresponse.text truncated to 500 characters, or "HTTP {code}" if empty

payload holds the decoded JSON body, or None when the body was not JSON.

Two errors are raised locally, before any request is made. Neither is a DiscolikeError, so except DiscolikeError does not catch them:

ExceptionRaised when
pydantic.ValidationErrorConstructing a request model with a value that fails a declared constraint, such as a number outside its range, a missing required field, or a Literal value not in the allowed set
ValueErrorappend() called without file and without query_id, or segment() called without domains and without query_id

discolike.ValidationError and pydantic.ValidationError are unrelated classes. The first is the server rejecting a request with HTTP 400 or 422 and carries status_code and payload; the second is pydantic rejecting the model before the client sends anything, and carries errors() and error_count() instead. Catch the one you mean:

import pydantic
from discolike.requests import MatchCompanyParams
try:
MatchCompanyParams(name="Acme", min_match_confidence=10)
except pydantic.ValidationError as exc:
print(exc.error_count(), exc.errors()[0]["loc"], exc.errors()[0]["msg"])
# 1 ('min_match_confidence',) Input should be greater than or equal to 50
from discolike import Discolike, AuthenticationError
client = Discolike(api_key="dl_not_a_real_key")
try:
client.account.usage()
except AuthenticationError as exc:
print(exc, exc.status_code, exc.payload)
# Invalid API Key or Session 401 {'detail': 'Invalid API Key or Session'}

The HTTP status codes behind these exceptions are documented in API Errors, and retry behavior is in Getting Started.