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
Section titled “account”Account usage and quota. Wraps the Usage endpoint.
account.usage
Section titled “account.usage”client.account.usage() -> UsageGET /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.37companies
Section titled “companies”Per-domain lookups. domain is required everywhere except extract, which takes either url or domain. Subdomains are normalized to the root domain server-side.
companies.data
Section titled “companies.data”client.companies.data(params: CompaniesDataParams) -> BizDataCompaniesDataParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Domain 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+ >1Bcompanies.score
Section titled “companies.score”client.companies.score(params: CompaniesScoreParams) -> ScoreCompaniesScoreParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Domain 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.0companies.growth
Section titled “companies.growth”client.companies.growth(params: CompaniesGrowthParams) -> GrowthCompaniesGrowthParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Domain 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.8companies.extract
Section titled “companies.extract”client.companies.extract(params: CompaniesExtractParams) -> ExtractResultCompaniesExtractParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
url | str | None | None | URL of the page to extract |
domain | str | None | None | Bare 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 Producompanies.redirects
Section titled “companies.redirects”client.companies.redirects(params: CompaniesRedirectsParams) -> list[Redirect]CompaniesRedirectsParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Query domain |
match | Literal['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)companies.vendors
Section titled “companies.vendors”client.companies.vendors(params: CompaniesVendorsParams) -> list[Vendor]CompaniesVendorsParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Query domain |
match | Literal['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)companies.subsidiaries
Section titled “companies.subsidiaries”client.companies.subsidiaries(params: CompaniesSubsidiariesParams) -> list[Subsidiary]CompaniesSubsidiariesParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Query domain |
match | str | 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 686companies.public_links
Section titled “companies.public_links”client.companies.public_links(params: CompaniesPublicLinksParams) -> list[PublicLink]CompaniesPublicLinksParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain | str | required | Domain to discover linkage for |
source | Literal['email', 'social', 'phone'] | required | Linkage 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)contacts
Section titled “contacts”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.
| Field | Type | Default | Description |
|---|---|---|---|
icp_prompt | str | None | None | Natural-language ICP description, up to 4000 characters. Extracts structured filters automatically; filters you set explicitly take precedence over extracted ones |
icp_text | str | None | None | Natural-language profile description for semantic matching, up to 4000 characters. Does not extract filters |
seniority | list[str] | None | None | Seniority levels: executive, vp, director, manager, senior_ic, mid_level, entry_level |
negate_seniority | list[str] | None | None | Seniority levels to exclude. Same values as seniority |
department | list[str] | None | None | Departments, 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_department | list[str] | None | None | Departments to exclude. Same values as department |
skills | list[str] | None | None | Required skills |
name | str | None | None | Contact name, partial match |
title | list[str] | None | None | Job title match terms. Supports quoted phrases and a + prefix for required terms; C-suite acronyms expand to their spelled-out forms unless quoted |
negate_title | list[str] | None | None | Job titles to exclude. Acronyms expand as in title |
summary | str | None | None | Semantic match against the profile summary |
negate_summary | str | None | None | Summary description to exclude |
person_country | list[str] | None | None | Contact country, ISO-3166-1 alpha-2 or a region alias |
negate_person_country | list[str] | None | None | Contact countries to exclude |
person_state | list[str] | None | None | Contact state/region. Scoped to a single person_country |
has_email | bool | None | False | Only contacts with an email address |
email_validated | bool | None | False | Only contacts whose email passed a deliverability check |
has_phone | bool | None | False | Only contacts with a phone number |
has_mobile | bool | None | False | Only contacts with at least one mobile number |
has_linkedin | bool | None | False | Only contacts with a LinkedIn profile |
min_connections | int | None | None | Minimum LinkedIn connections, 0 or more |
jobstart_date | str | None | None | Minimum date YYYY-MM-DD, or a range YYYY-MM-DD,YYYY-MM-DD. Contacts without a known start date are excluded |
persona_id | list[int] | None | None | Persona IDs to use as the similarity basis |
domain | list[str] | None | None | Restrict to contacts at these company domains |
filter_industry | list[str] | None | None | Company industries, each one of the industry categories listed on the Industries page |
negate_filter_industry | list[str] | None | None | Company industries to exclude. Same values as filter_industry |
filter_country | list[str] | None | None | Company country, ISO-3166-1 alpha-2 or a region alias |
negate_filter_country | list[str] | None | None | Company countries to exclude |
filter_state | list[str] | None | None | Company state/region. Scoped to a single filter_country |
negate_filter_state | list[str] | None | None | Company states to exclude |
employee_range | str | None | None | Company size as 'min,max', e.g. '51,200' |
inclusion_query_id | list[str] | None | None | Saved query IDs to include. See queries |
exclusion_query_id | list[str] | None | None | Saved query IDs to exclude |
max_records | int | None | 100 | Maximum contacts to return, 20-10000 |
max_companies | int | None | None | Maximum enriched companies to return, 1-10000. Do not combine with max_records |
offset | int | None | 0 | Records to skip, 0-10000 |
results_by_company | int | None | 5 | Maximum contacts per company domain, 0-100. 0 removes the per-company cap. A non-zero value forces offset to 0 |
include_search_contacts | bool | None | False | Include contacts from the search index for broader coverage |
consensus | int | None | 1 | Number of query vectors combined for consensus search, 1-20 |
contacts.search
Section titled “contacts.search”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.comcontacts.count
Section titled “contacts.count”client.contacts.count(params: ContactsCountParams) -> CountContactsCountParams 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)# 978contacts.lookup
Section titled “contacts.lookup”client.contacts.lookup(params: ContactsLookupParams) -> ContactContactsLookupParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
persona_id | int | None | None | Persona ID to resolve |
linkedin | str | None | None | LinkedIn profile URL or username to resolve |
email | str | None | None | Email 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.comcontacts.match
Section titled “contacts.match”client.contacts.match(params: ContactsMatchParams) -> ContactMatchResponseContactsMatchParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
name | str | required | Person’s name |
company_name | str | None | None | Company name to narrow the search |
domain | str | None | None | Company domain to narrow the search |
person_country | str | None | None | Contact country, ISO-3166-1 alpha-2, to narrow the search |
limit | int | None | 10 | Maximum 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 Institutecontacts.bulk_match
Section titled “contacts.bulk_match”client.contacts.bulk_match(request: BulkContactMatchRequest) -> JobBulkContactMatchRequest is imported from discolike.requests, as is BulkContactMatchQueryItem for each entry in queries.
| Field | Type | Default | Description |
|---|---|---|---|
queries | list[BulkContactMatchQueryItem] | required | Match queries, 1-10000 items |
enrich | bool | None | False | Hydrate full contact data for each match. Costs credits |
limit | int | None | 10 | Maximum candidates per query, 1-20 |
BulkContactMatchQueryItem:
| Field | Type | Default | Description |
|---|---|---|---|
name | str | None | None | Person’s name. Required unless email is set |
email | str | None | None | Email address for exact lookup. When found, name and company matching is skipped for this row |
company_name | str | None | None | Company name to narrow the search |
domain | str | None | None | Company domain to narrow the search |
person_country | str | None | None | Contact 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)contacts.discover
Section titled “contacts.discover”client.contacts.discover(request: ContactFilters) -> ContactsDiscoverResponseContactFilters 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)contacts.generate
Section titled “contacts.generate”client.contacts.generate(request: ContactGenerateRequest) -> JobContactGenerateRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
icp_text | str | required | Natural-language description of the contacts to generate |
domains | list[str] | required | Domains to generate contacts for, 1-10000 items |
context_mode | Literal['website', 'profile', 'domain'] | None | 'website' | What the model sees per domain |
integration_id | str | None | None | LLM provider integration UUID. Your default is used when omitted |
search_provider_id | str | None | None | Search provider integration UUID. Your default is used when omitted |
search_context_size | Literal['low', 'medium', 'high'] | None | 'low' | Search queries per record |
max_contacts_per_domain | int | None | 10 | Cap on contacts generated per domain |
max_company_records | int | None | None | Cap on companies processed |
full_domains | list[str] | None | None | Accepted by the platform; undocumented in the spec |
partial_domains | list[str] | None | None | Accepted by the platform; undocumented in the spec |
initial_contact_counts | dict[str, int] | None | None | Per-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)discogen
Section titled “discogen”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.
discogen.process
Section titled “discogen.process”client.discogen.process(request: DiscoGenProcessRequest) -> JobDiscoGenProcessRequest 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.
| Field | Type | Default | Description |
|---|---|---|---|
query | str | required | The prompt to run against each record. At least 1 character |
domains | list[str] | required | Domains to process, 1-10,000 items |
integration_id | str | None | None | LLM provider integration UUID. Your default LLM provider is used if omitted |
web_search | bool | None | False | Enable web search enrichment |
context_mode | str | None | 'website' | What the model sees per domain: website, profile, or domain |
include_x_search | bool | None | False | Include X (Twitter) results for xAI models |
search_provider_id | str | None | None | Search provider integration UUID. Your default search provider is used if omitted |
search_context_size | str | None | 'low' | Search queries per record: low, medium, or high |
previous_discogen_data | dict[str, Any] | None | None | Passed 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)discogen.process_personas
Section titled “discogen.process_personas”client.discogen.process_personas(request: DiscoGenPersonaProcessRequest) -> JobDiscoGenPersonaProcessRequest is imported from discolike.requests.
Identical to DiscoGenProcessRequest except for two fields:
| Field | Type | Default | Description |
|---|---|---|---|
persona_ids | list[int] | required | Contact record IDs to process, 1-10,000 items |
context_mode | str | 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()discogen.models
Section titled “discogen.models”client.discogen.models() -> DiscogenModelsGET /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}]discogen.job
Section titled “discogen.job”client.discogen.job(task_id: str) -> Job| Parameter | Type | Description |
|---|---|---|
task_id | str | Task 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)discovery
Section titled “discovery”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.
discover
Section titled “discover”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:
| Field | Type | Default | Description |
|---|---|---|---|
domain | list[str] | None | None | Example domains for lookalike matching, up to 10 |
exclude_domain | list[str] | None | None | Hard-exclude specific domains from results without affecting lookalike matching, up to 100 |
icp_text | str | None | None | Natural-language description of your ideal customer profile, 3-4000 characters |
icp_prompt | str | None | None | Natural-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:
| Field | Type | Default | Description |
|---|---|---|---|
phrase_match | list[str] | None | None | Exact text fragments to match in site content, up to 20, each at least 3 characters |
negate_phrase_match | list[str] | None | None | Exact text fragments to exclude, up to 20, each at least 3 characters |
Location:
| Field | Type | Default | Description |
|---|---|---|---|
country | list[str] | None | None | ISO-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_country | list[str] | None | None | Countries to exclude, same codes and region aliases as country |
state | list[str] | None | None | ISO 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_state | list[str] | None | None | States to exclude, same format as state, up to 100 |
Company filters:
| Field | Type | Default | Description |
|---|---|---|---|
category | list[str] | None | None | Industry filter (SOFTWARE, HEALTHCARE, …), validated locally against the industry list. See Industries |
negate_category | list[str] | None | None | Industries to exclude, same values as category |
employee_range | str | None | None | min,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_range | str | None | None | min,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_model | list[str] | None | None | B2B, B2C, B2G, G2B, G2C, D2C, C2C, C2B (OR semantics) |
negate_business_model | list[str] | None | None | Business-model labels to exclude, same values as business_model |
start_date | str | None | None | YYYY-MM-DD, or a range YYYY-MM-DD,YYYY-MM-DD |
min_digital_footprint | int | None | None | Minimum digital footprint score, 0-800. Server default 50 when omitted |
max_digital_footprint | int | None | None | Maximum digital footprint score, 0-800. Server default 800 when omitted |
exclude_leadgen | bool | None | True | Exclude suspected lead-generation sites: profiles scoring 25 or below with no phone, email, or social presence |
Social, language, redirects:
| Field | Type | Default | Description |
|---|---|---|---|
social | list[str] | None | None | Require 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_social | list[str] | None | None | Exclude companies with these social profiles, same values as social |
language | list[str] | None | None | Site language, 2-letter codes, validated locally against the language list. See Languages |
negate_language | list[str] | None | None | Languages to exclude, same values as language |
redirect | bool | None | False | Include domains that redirect elsewhere |
Tech stack and subdomains:
| Field | Type | Default | Description |
|---|---|---|---|
tech_stack | list[str] | None | None | Limit to companies using these vendor domains, up to 20 |
negate_tech_stack | list[str] | None | None | Exclude companies using these vendor domains, up to 20 |
subdomain | list[str] | None | None | Limit results to these subdomains, up to 20, each at least 3 characters |
negate_subdomain | list[str] | None | None | Exclude these subdomains, up to 20, each at least 3 characters |
Saved queries. Create and manage the underlying queries with client.queries:
| Field | Type | Default | Description |
|---|---|---|---|
inclusion_query_id | list[str] | None | None | Limit results to domains from these saved query results. Requires the STARTER plan |
exclusion_query_id | list[str] | None | None | Exclude domains from these saved query results |
Result controls:
| Field | Type | Default | Description |
|---|---|---|---|
max_records | int | None | 100 | Records to return, 5-10,000. Values outside the range are rejected locally |
offset | int | None | 0 | Records to skip, for pagination. 0 or greater |
min_similarity | int | None | 0 | Minimum similarity score, 0-99 |
consensus | int | None | 1 | Number of top results forming the consensus search vector, 1-20. Higher values reduce specificity |
variance | str | None | 'UNRESTRICTED' | Result diversity: LOW, MID_LOW, MEDIUM, MID_HIGH, HIGH, UNRESTRICTED |
include_search_domains | bool | None | False | Include the input domain values in the results |
AI features:
| Field | Type | Default | Description |
|---|---|---|---|
enhanced | bool | None | False | AI-powered result enhancement for relevance |
retrieval | bool | None | False | Retrieve page data using the Extract API |
auto_icp_text | bool | None | False | Generate ICP text automatically from the provided domain values |
auto_phrase_match | bool | None | False | Generate 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-50client.count(params: CountParams) -> CountCountParams 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)# 1651Finds 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.
email.find
Section titled “email.find”client.email.find(request: FindEmailRequest) -> EmailJobFindEmailRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
first_name | str | required | First name of the person. |
last_name | str | required | Last name of the person. |
domain | str | required | Company domain to search, e.g. acme.com. |
known_pattern | str | None | None | Known 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.lastemail.find_batch
Section titled “email.find_batch”client.email.find_batch(request: FindEmailBatchRequest) -> EmailBatchFindEmailBatchRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
requests | list[FindEmailRequest] | required | People 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)email.job
Section titled “email.job”client.email.job(job_id: str, *, kind: Literal["find", "verify"] = "find") -> EmailJob| Parameter | Type | Description |
|---|---|---|
job_id | str | Job ID to rehydrate. Positional, not keyword |
kind | Literal["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.comemail.batch
Section titled “email.batch”client.email.batch(batch_id: str, *, kind: Literal["find", "verify"]) -> EmailBatch| Parameter | Type | Description |
|---|---|---|
batch_id | str | Batch ID to rehydrate. Positional, not keyword |
kind | Literal["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)enrich
Section titled “enrich”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.
append
Section titled “append”client.append(params: AppendParams, *, file: pathlib.Path | str | BinaryIO | None = None) -> list[AppendResult] | bytesAppendParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
dataset | list[str] | required | Datasets to append, at least one of bizdata, redirects, domain_status, growth, vendors. See the table below |
query_id | list[str] | None | None | Saved-query IDs to resolve into domains, unioned with any domains from file. Optional if file is given |
domain_column | str | None | 'domain' | Column in file holding domains |
csv | bool | None | False | Return CSV bytes instead of parsed JSON rows |
file | pathlib.Path | str | BinaryIO | None | None | Keyword-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:
| Value | What it appends |
|---|---|
bizdata | Full BizData firmographic profile: name, status, score, description, address, keywords, industry_groups, and the rest |
domain_status | status_code, status_reason, record_date |
redirects | redirect_sources (capped at 1,000 domains), redirect_count |
growth | Quarterly score_* and subdomains_* fields, score_growth_3m, subdomain_growth_3m |
vendors | vendors: 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+ >1Bsegment
Section titled “segment”client.segment(params: SegmentParams) -> JobSegmentParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domains | str | None | '' | Comma-separated domains to segment |
query_id | list[str] | None | None | Saved-query IDs whose resolved domains are segmented |
max_segments | int | None | None | Maximum 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:
| Field | Type | Description |
|---|---|---|
segment_id | Integer | Assigned segment. -1 means unclustered |
segment_description | String | Generated description of the segment. UNCLUSTERED for segment -1 |
probability | Float | Confidence 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"])segment_file
Section titled “segment_file”client.segment_file(params: SegmentFileParams, *, file: pathlib.Path | str | BinaryIO) -> JobSegmentFileParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
domain_column | str | None | 'domain' | Column in file holding domains |
max_segments | int | None | None | Maximum number of segments to create, 2-100. Chosen automatically when omitted |
query_id | list[str] | None | None | Saved-query IDs whose resolved domains are segmented alongside the file’s |
file | pathlib.Path | str | BinaryIO | required | Keyword-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"])validate_icp
Section titled “validate_icp”client.validate_icp(request: ValidateIcpRequest) -> JobValidateIcpRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
icp_text | str | required | Your ICP description |
domains | list[str] | required | Domains to validate, 1-10,000 items |
context_mode | Literal['website', 'profile', 'domain'] | None | 'website' | What the model sees per domain: website (profile plus homepage), profile (firmographics only), domain (name only) |
integration_id | str | None | None | LLM provider integration UUID. Your default is used when omitted |
web_search | bool | None | False | Enable web search enrichment |
search_provider_id | str | None | None | Search 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:
| Column | Values | Description |
|---|---|---|
Fit | yes, partial, no | Whether the company matches the ICP |
Confidence | high, medium, low | How confident the assessment is |
Reasoning | String | One 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)llm_providers
Section titled “llm_providers”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.
llm_providers.list
Section titled “llm_providers.list”client.llm_providers.list() -> LLMProviderListGET /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 searchllm_providers.get
Section titled “llm_providers.get”client.llm_providers.get(*, integration_id: str) -> LLMProviderConfig| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration 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())llm_providers.create
Section titled “llm_providers.create”client.llm_providers.create(request: LLMProviderCreateRequest) -> LLMIntegrationResultLLMProviderCreateRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
integration_name | str | required | Display name for the integration |
provider | str | required | Provider slug, e.g. openai, anthropic, xai, gemini |
api_key | str | required | Your provider API key |
model_name | str | required | Model identifier, e.g. xai/grok-4.5-latest. Valid values come from client.discogen.models() |
base_url | str | None | None | Override 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)llm_providers.update
Section titled “llm_providers.update”client.llm_providers.update(request: LLMProviderUpdateRequest, *, integration_id: str) -> LLMIntegrationResultLLMProviderUpdateRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
integration_name | str | required | Display name |
provider | str | required | Provider slug |
api_key | str | None | required | Your provider API key. Must be passed; None is sent as an explicit null and keeps the stored key |
model_name | str | required | Model identifier |
base_url | str | None | None | Override the provider endpoint |
integration_id | str | required | Keyword-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",)llm_providers.delete
Section titled “llm_providers.delete”client.llm_providers.delete(*, integration_id: str) -> None| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration UUID. Required |
DELETE /llm-providers/config/{integration_id}. Returns None.
client.llm_providers.delete(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003")llm_providers.set_default
Section titled “llm_providers.set_default”client.llm_providers.set_default(*, integration_id: str) -> LLMIntegrationResult| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration 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)llm_providers.test_connection
Section titled “llm_providers.test_connection”client.llm_providers.test_connection(request: LLMProviderCreateRequest) -> LLMIntegrationResultTakes 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.
match.company
Section titled “match.company”client.match.company(params: MatchCompanyParams) -> MatchResponseMatchCompanyParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
name | str | required | Company name to match |
phone | str | None | None | Phone number to disambiguate the match, E.164 or local format |
city | str | None | None | City to disambiguate the match |
state | str | None | None | State or region to disambiguate the match |
country | str | None | None | ISO-3166-1 alpha-2 country code to disambiguate the match |
zip_code | str | None | None | ZIP or postal code to disambiguate the match |
strict | bool | None | False | Strict matching, no filter relaxation |
local_mode | bool | None | False | Preserve location filters during relaxation |
min_match_confidence | int | None | 50 | Minimum 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.0match.bulk
Section titled “match.bulk”client.match.bulk(params: MatchBulkParams, *, file: pathlib.Path | str | BinaryIO) -> JobMatchBulkParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
name_column | str | required | Column holding company names |
phone_column | str | None | None | Column holding phone numbers |
city_column | str | None | None | Column holding city names |
state_column | str | None | None | Column holding state codes |
country_column | str | None | None | Column holding country codes |
zip_code_column | str | None | None | Column holding zip codes |
strict | bool | None | False | Strict matching, no filter relaxation |
local_mode | bool | None | False | Preserve location filters during relaxation |
min_match_confidence | int | None | 50 | Minimum match_confidence a match must have, 50-100 |
file | pathlib.Path | str | BinaryIO | required | Keyword-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 pass | Upload filename | Who closes the handle |
|---|---|---|
pathlib.Path | The path’s basename | The SDK opens and closes it for you |
str path | The path’s basename | The SDK opens and closes it for you |
| Open binary handle | The handle’s .name basename, or upload.csv if it has none | You 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)queries
Section titled “queries”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.
queries.list
Section titled “queries.list”client.queries.list(params: QueriesListParams) -> SavedQueriesQueriesListParams is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
max_records | int | None | 100 | Maximum records to return, 1-1000 |
offset | int | None | 0 | Records to skip, for pagination. 0 or more |
action | str | None | None | Filter by action type, e.g. discover, exclusion. Matches partially, so discover also returns thin_discover rows |
tags | list[str] | None | None | Filter 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 100queries.create_exclusion_list
Section titled “queries.create_exclusion_list”client.queries.create_exclusion_list(request: CreateExclusionListRequest) -> QueryResultCreateExclusionListRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
query_name | str | required | Name for the saved list, 1-255 characters |
domains | list[str] | None | None | Domains to exclude |
persona_ids | list[int] | None | None | Persona IDs to exclude |
tags | list[str] | None | None | Tags 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)queries.save_results
Section titled “queries.save_results”client.queries.save_results(request: SaveResultsRequest) -> QueryResultSaveResultsRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
query_name | str | required | Name for the saved query, 1-255 characters |
action | str | required | Action type to record against the query. One of discover, segment, contacts, append, match |
data | list[dict[str, Any]] | required | The rows to save. At least one row |
query_params | dict[str, Any] | None | None | Arbitrary parameters to store alongside the query, for provenance |
domain_column | str | None | 'domain' | Which key in each row holds the domain. Up to 128 characters |
tags | list[str] | None | None | Tags 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)queries.update
Section titled “queries.update”client.queries.update(request: UpdateQueryRequest, *, query_id: str) -> QueryResultUpdateQueryRequest is imported from discolike.requests.
| Field | Type | Default | Description |
|---|---|---|---|
query_name | str | None | None | New display name, up to 255 characters |
tags | list[str] | None | None | Replacement tag list, up to 20 items |
query_id | str | required | Keyword-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")queries.delete
Section titled “queries.delete”client.queries.delete(*, query_id: str) -> None| Parameter | Type | Description |
|---|---|---|
query_id | str | Query 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")search_providers
Section titled “search_providers”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.
search_providers.list
Section titled “search_providers.list”client.search_providers.list() -> SearchProviderListGET /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 Searchsearch_providers.create
Section titled “search_providers.create”client.search_providers.create(request: SearchProviderRequest) -> SearchProviderConfigSearchProviderRequest is imported from discolike.requests. update() takes the same model.
| Field | Type | Default | Description |
|---|---|---|---|
integration_name | str | required | Display name for the integration |
provider | str | required | Provider slug, e.g. serper, linkup, tavily |
search_model | str | required | Model identifier, e.g. serper/search. Get valid values from models() |
api_key | str | None | None | Your provider API key. Not sent when unset |
base_url | str | None | None | Override 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)search_providers.update
Section titled “search_providers.update”client.search_providers.update(request: SearchProviderRequest, *, integration_id: str) -> SearchProviderConfigSearchProviderRequest is imported from discolike.requests; it is the same model create() takes.
| Field | Type | Default | Description |
|---|---|---|---|
integration_name | str | required | Display name |
provider | str | required | Provider slug |
search_model | str | required | Model identifier |
api_key | str | None | None | Your provider API key. Leave unset, or pass None, to keep the stored key |
base_url | str | None | None | Override the provider endpoint |
integration_id | str | required | Keyword-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",)search_providers.delete
Section titled “search_providers.delete”client.search_providers.delete(*, integration_id: str) -> None| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration UUID. Required |
DELETE /search-providers/{integration_id}. Returns None.
client.search_providers.delete(integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb")search_providers.set_default
Section titled “search_providers.set_default”client.search_providers.set_default(*, integration_id: str) -> SearchProviderResult| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration 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)search_providers.clear_default
Section titled “search_providers.clear_default”client.search_providers.clear_default(*, integration_id: str) -> SearchProviderResult| Parameter | Type | Description |
|---|---|---|
integration_id | str | Integration UUID. Required |
DELETE /search-providers/{integration_id}/default. Returns SearchProviderResult.
client.search_providers.clear_default( integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb")search_providers.models
Section titled “search_providers.models”client.search_providers.models() -> SearchModelsGET /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}]signup
Section titled “signup”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
Section titled “signup”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) -> SignupResultPOST /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.
| Argument | Type | Default | Description |
|---|---|---|---|
email | str | required | The person’s work email. Free-mail and disposable domains are rejected |
first_name | str | required | 1-40 characters after NFC normalization and trimming. Must contain a letter and no angle brackets or control characters |
last_name | str | required | Same rules as first_name |
agent | str | None | None | Agent or framework name recorded with the signup. Defaults to discolike-python/<version> |
base_url | str | https://api.discolike.com/v1 | API base URL |
timeout | float | 60.0 | Request timeout in seconds |
http_client | Client | None | None | Reuse an existing HTTP client instead of opening one. Not closed for you |
allow_new_email | bool | False | Sign 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.async_signup
Section titled “async_signup”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) -> SignupResultIdentical to signup, awaited. http_client takes an async client.
Models, credentials, job handles, and exceptions. Response models subclass DiscolikeModel; request models subclass DiscolikeRequest.
DiscolikeModel and extra fields
Section titled “DiscolikeModel and extra fields”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.
| Access | Result |
|---|---|
model.to_dict() | The full response as a plain JSON-mode dict, extras included. Use this by default. |
model.model_extra | A 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
Section titled “DiscolikeRequest”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
Noneexplicitly is kept in the output. JSON-body routes send it asnull; 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 DiscolikeRequestfrom discolike.requests import MatchCompanyParamsThe full set of models, by namespace:
| Namespace | Model | Method |
|---|---|---|
| root | DiscoverParams | discover() |
| root | CountParams | count() |
| root | ValidateIcpRequest | validate_icp() |
| root | AppendParams | append() |
| root | SegmentParams | segment() |
| root | SegmentFileParams | segment_file() |
| companies | CompaniesDataParams | companies.data() |
| companies | CompaniesScoreParams | companies.score() |
| companies | CompaniesGrowthParams | companies.growth() |
| companies | CompaniesExtractParams | companies.extract() |
| companies | CompaniesRedirectsParams | companies.redirects() |
| companies | CompaniesVendorsParams | companies.vendors() |
| companies | CompaniesSubsidiariesParams | companies.subsidiaries() |
| companies | CompaniesPublicLinksParams | companies.public_links() |
| match | MatchCompanyParams | match.company() |
| match | MatchBulkParams | match.bulk() |
| contacts | ContactsSearchParams | contacts.search() |
| contacts | ContactsCountParams | contacts.count() |
| contacts | ContactsLookupParams | contacts.lookup() |
| contacts | ContactsMatchParams | contacts.match() |
| contacts | BulkContactMatchRequest | contacts.bulk_match() |
| contacts | BulkContactMatchQueryItem | One entry of BulkContactMatchRequest.queries |
| contacts | ContactFilters | contacts.discover() |
| contacts | ContactGenerateRequest | contacts.generate() |
| discogen | DiscoGenProcessRequest | discogen.process() |
| discogen | DiscoGenPersonaProcessRequest | discogen.process_personas() |
FindEmailRequest | email.find() | |
FindEmailBatchRequest | email.find_batch() | |
| queries | QueriesListParams | queries.list() |
| queries | CreateExclusionListRequest | queries.create_exclusion_list() |
| queries | SaveResultsRequest | queries.save_results() |
| queries | UpdateQueryRequest | queries.update() |
| search_providers | SearchProviderRequest | search_providers.create() and update() |
| llm_providers | LLMProviderCreateRequest | llm_providers.create() and test_connection() |
| llm_providers | LLMProviderUpdateRequest | llm_providers.update() |
CompanyProfile
Section titled “CompanyProfile”The base profile model. BizData (from companies.data) is CompanyProfile with no added fields; Company and MatchResult each add one.
| Field | Type | Description |
|---|---|---|
domain | str | None | Normalized domain, the unique identifier |
name | str | None | Company name from certificate or website |
status | CompanyStatus | None | Operating status with confidence |
score | int | None | Digital footprint score (1-800) |
start_date | str | None | First certificate date, an estimate of company start |
end_date | str | None | Last certificate date if closed, None if active |
address | CompanyAddress | None | HQ address |
phones | list[str] | None | Phone numbers from the website |
public_emails | list[str] | None | Contact emails from the website |
domain_associations | list[str] | Associated domains. Defaults to [] |
social_urls | list[str] | None | Social profile URLs |
redirect_domain | str | None | Final domain if the site redirects |
description | str | None | Company description from the website |
keywords | dict[str, float] | Keyword to confidence score. Defaults to {} |
industry_groups | dict[str, float] | Industry classification to score. Defaults to {} |
employees | str | None | Employee bucket: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+ |
revenue_range | str | None | Revenue bucket: <1M, 1-10M, 10-100M, 100M-1B, >1B, or N/A |
business_model | dict[str, float] | Business-model label to confidence. Defaults to {} |
update_date | str | None | Last record update |
mx_provider | str | None | Mail host domain, no_mx if the domain has no mail server, None if unresolved |
linkup | None | Annotated 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.
Company
Section titled “Company”Returned by discover(). CompanyProfile plus one field:
| Field | Type | Description |
|---|---|---|
similarity | float | None | Similarity between the result and your query (0-100) |
Returned by count().
| Field | Type | Description |
|---|---|---|
count | int | None | Number of companies matching the filters |
Returned by companies.score().
| Field | Type | Description |
|---|---|---|
domain | str | None | Normalized domain |
score | int | None | Composite score, 0-800 |
parameters | ScoreParameters | None | Score components |
first_event | str | None | Date 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.
Growth
Section titled “Growth”Returned by companies.growth().
| Field | Type | Description |
|---|---|---|
domain | str | None | Normalized domain |
score_growth_3m | float | None | Score growth rate over the last three months |
subdomain_growth_3m | float | None | Subdomain growth rate over the last three months |
The API also returns one score_YYYYQX and one subdomains_YYYYQX key per quarter, as extras.
ExtractResult
Section titled “ExtractResult”Returned by companies.extract().
| Field | Type | Description |
|---|---|---|
text | str | None | Extracted page text |
language | str | None | Detected language code |
Redirect
Section titled “Redirect”Returned by companies.redirects().
| Field | Type | Description |
|---|---|---|
source_domain | str | None | Normalized source domain |
source_fqdn | str | None | Full source URL |
linked_domain | str | None | Normalized destination domain |
linked_fqdn | str | None | Full destination URL |
record_date | str | None | Date the record was compiled |
Vendor
Section titled “Vendor”Returned by companies.vendors().
| Field | Type | Description |
|---|---|---|
client_domain | str | None | Normalized client domain |
client_fqdn | str | None | Client’s full URL |
vendor_domain | str | None | Normalized vendor domain |
vendor_fqdn | str | None | Vendor’s full URL |
record_date | str | None | Date the record was compiled |
Subsidiary
Section titled “Subsidiary”Returned by companies.subsidiaries().
| Field | Type | Description |
|---|---|---|
source_domain | str | None | Normalized source domain |
source_fqdn | str | None | Full source URL |
source_score | int | None | Source domain score (1-800) |
linked_domain | str | None | Normalized linked domain |
linked_fqdn | str | None | Full linked URL |
linked_score | int | None | Linked domain score (1-800) |
parent_domain | str | None | Domain with the highest score |
child_domain | str | None | Subsidiary domain |
record_date | str | None | Date the record was compiled |
PublicLink
Section titled “PublicLink”Returned by companies.public_links().
| Field | Type | Description |
|---|---|---|
domain | str | None | Normalized query domain |
linked_domain | str | None | Related domain found |
link_values | list[str] | Shared contact values establishing the link. Defaults to [] |
record_date | str | None | Date the record was compiled |
Contact
Section titled “Contact”Returned by contacts.search() and contacts.lookup().
| Field | Type |
|---|---|
persona_id | int | None |
domain | str | None |
name | str | None |
title | str | None |
email | str | 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.
ContactMatchResponse
Section titled “ContactMatchResponse”Returned by contacts.match().
| Field | Type | Description |
|---|---|---|
query | ContactMatchQuery | None | The query the server ran, echoed back |
matches | list[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().
| Field | Type | Description |
|---|---|---|
results | dict[str, ContactsByCompany] | Keyed by company domain. Defaults to {} |
total_contacts | int | None | Matching contacts across all pages, not just this batch |
total_domains | int | None | Unique 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).
MatchResponse
Section titled “MatchResponse”Returned by match.company().
| Field | Type | Description |
|---|---|---|
query | MatchQuery | None | The query the API echoed back |
matches | list[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.
MatchResult
Section titled “MatchResult”CompanyProfile plus one field:
| Field | Type | Description |
|---|---|---|
match_confidence | float | None | Match confidence, 0-100 |
AppendResult
Section titled “AppendResult”Returned by append() when the response is JSON.
| Field | Type | Description |
|---|---|---|
domain | str | None | The 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.
EnumerationOutput / EnumerationMatch
Section titled “EnumerationOutput / EnumerationMatch”What email.find() produces.
| Field | Type |
|---|---|
first_name, last_name, domain, status, mx_host, provider, error | str | None |
result | EnumerationMatch | None |
is_catch_all | bool | None |
attempts, duration_ms | int | None |
EnumerationMatch, the address itself:
| Field | Type |
|---|---|
email, pattern | str | None |
tier, smtp_code | int | None |
valid | bool | None |
For the meaning of each status value, see Email Find.
ValidationOutput
Section titled “ValidationOutput”What a "verify" batch produces.
| Field | Type |
|---|---|
email, status, mx_host, provider, error, reason | str | None |
is_deliverable, is_catch_all | bool | None |
smtp_code, attempts, duration_ms | int | None |
EmailJobResult
Section titled “EmailJobResult”The per-job wrapper.
| Field | Type |
|---|---|
job_id | str | None |
status | str | None |
result | EnumerationOutput | ValidationOutput | None |
error | str | 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 []).
DiscogenModels / DiscogenModelInfo
Section titled “DiscogenModels / DiscogenModelInfo”Returned by discogen.models().
| Field | Type | Description |
|---|---|---|
models | dict[str, list[DiscogenModelInfo]] | Model lists keyed by provider name. Defaults to {} |
DiscogenModelInfo: name: str | None, supports_web_search: bool | None.
SavedQueries
Section titled “SavedQueries”Returned by queries.list().
| Field | Type | Description |
|---|---|---|
results | list[SavedQuery] | The page of saved queries. Defaults to [] |
count | int | None | Total matching queries on the server |
SavedQuery
Section titled “SavedQuery”| Field | Type | Description |
|---|---|---|
query_id | str | None | UUID to pass downstream as query_id |
query_name | str | None | Display name |
action | str | None | Action type that produced the query |
user_name | str | None | Who created it |
mtime | str | None | Last modified timestamp, ISO 8601 |
domains | list[str] | None | Domains attached to the query |
domain_count | int | None | Number of domains |
persona_id_count | int | None | Number of personas |
tags | list[str] | Attached tags. Defaults to [] |
The API also returns query_params on each row (the stored parameters that produced the query) as an extra.
QueryResult
Section titled “QueryResult”Returned by create_exclusion_list, save_results, and update.
| Field | Type | Description |
|---|---|---|
query_id | str | None | UUID of the created or updated query |
query_name | str | None | Display name |
action | str | None | Action type recorded against the query |
domain_count | int | None | Number of domains |
persona_id_count | int | None | Number of personas |
row_count | int | None | Number of rows saved |
tags | list[str] | None | Attached tags |
SearchProviderList
Section titled “SearchProviderList”| Field | Type | Description |
|---|---|---|
providers | list[SearchProviderConfig] | Configured search integrations. Defaults to [] |
SearchProviderConfig
Section titled “SearchProviderConfig”| Field | Type | Description |
|---|---|---|
integration_id | str | None | UUID to pass as search_provider_id |
integration_name | str | None | Display 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}SearchProviderResult
Section titled “SearchProviderResult”| Field | Type | Description |
|---|---|---|
message | str | None | Server message |
integration_id | str | None | Affected integration |
SearchModels / SearchModelInfo
Section titled “SearchModels / SearchModelInfo”| Field | Type | Description |
|---|---|---|
models | dict[str, list[SearchModelInfo]] | Model lists keyed by provider. Defaults to {} |
SearchModelInfo: name: str | None, cost_per_query: float | None (USD per search query).
LLMProviderList
Section titled “LLMProviderList”| Field | Type | Description |
|---|---|---|
providers | list[LLMProviderConfig] | Configured LLM integrations. Defaults to [] |
mtime | str | None | Last modified timestamp of the configuration set |
LLMProviderConfig
Section titled “LLMProviderConfig”| Field | Type | Description |
|---|---|---|
integration_id | str | None | UUID to pass as integration_id on DiscoGen calls |
integration_name | str | None | Display 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}LLMIntegrationResult
Section titled “LLMIntegrationResult”| Field | Type | Description |
|---|---|---|
message | str | None | Server message |
integration_id | str | None | Affected integration |
status | str | None | Result status |
Returned by account.usage(). It declares three fields, and the live API returns none of them:
| Field | Type | Description |
|---|---|---|
requests_mtd | int | None | None against the live API |
records_mtd | int | None | None against the live API |
spend_mtd | float | None | None against the live API |
The month-to-date values arrive under different names, as extras:
| Extra field | Type | Description |
|---|---|---|
month_to_date_requests | int | Requests made this month |
month_to_date_records | int | Records returned this month |
month_to_date_spend | float | Spend this month, in USD |
account_status | str | Account state, e.g. active |
max_spend | str | float | Spend cap, or "unlimited" |
total_available_spend | str | float | Remaining spend, or "unlimited" |
carryover_credits | float | Credits carried over from the previous period |
top_up_credits | float | Credits added by top-up |
usage_summary | dict | Per-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_events | list[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_billed | int | Companies billed in the recent window |
recent_contacts_billed | int | Contacts billed in the recent window |
estimated_provider_spend_recent | float | Estimated BYOK provider spend in the recent window |
data = client.account.usage().to_dict()print(data["month_to_date_spend"], data["account_status"])# 26.37 activeSignupResult
Section titled “SignupResult”Returned by signup and async_signup.
| Field | Type | Description |
|---|---|---|
status | str | created |
email | str | Normalized email the account was created for |
org_domain | str | Organization domain derived from the email |
org_status | str | created when a new organization was made, joined when one already existed for the domain |
next_step | str | Instruction to relay to the account owner |
ApiKeyCredential and OAuthCredential
Section titled “ApiKeyCredential and OAuthCredential”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", ))| Class | Field | Type | Description |
|---|---|---|---|
ApiKeyCredential | api_key | str | Sent as the X-discolike-key header on every request |
OAuthCredential | access_token | str | Sent as Authorization: Bearer on every request |
refresh_token | str | Exchanged at token_endpoint when the access token is about to expire or a request returns 401 | |
expires_at | float | Unix timestamp the access token expires at. The client refreshes within 60 seconds of it | |
client_id | str | The OAuth client the tokens were issued to; sent with every refresh | |
token_endpoint | str | Where 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.
Job and AsyncJob
Section titled “Job and AsyncJob”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: ...| Member | Type | Description |
|---|---|---|
task_family | str | One of bulkmatch, contactmatch, discogen, segment. Determines the polling path |
task_id | str | Server-assigned task identifier. Store this to resume polling in another process |
status() | -> JobStatus | One GET /{task_family}/status/{task_id} request. Returns the current status without blocking |
cancel() | -> None | DELETE /{task_family}/cancel/{task_id}. Returns as soon as the request is accepted; the task stops asynchronously |
wait() | -> JobStatus | Polls status() until the task reaches a terminal status, then returns that status |
wait() parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout | float | 900.0 | Seconds before JobTimeoutError is raised. Measured on a monotonic clock, so it is unaffected by system clock changes |
poll_interval | float | 5.0 | Fixed seconds slept between fetches |
on_poll | Callable[[JobStatus], None] | None | None | Called 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()raisesJobFailedError. The message isstr(status.result), falling back to"task failed";exc.payloadis the full status dict. - On
status == "completed"or"cancelled",wait()returns theJobStatus. A cancelled task returns with empty results, so checkstatus.statusbefore using them. - On timeout,
wait()raisesJobTimeoutError. The task keeps running server-side. Callwait()again on the same handle to resume, or pollstatus()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)JobStatus
Section titled “JobStatus”| Attribute | Type | Description |
|---|---|---|
status | str | Required. Terminal values are completed, failed, cancelled. Anything else means still running |
progress | int | None | Percent complete, when the server reports it |
results | Any | The list-shaped payload for a finished task |
result | Any | The scalar payload, and also where a failure message lands |
warnings | list[str] | Defaults to an empty list |
estimated_cost | float | None | DiscoGen family only. Best-effort spend on your own provider keys |
cost_metadata | dict[str, dict[str, Any]] | None | DiscoGen 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.
EmailJob and EmailBatch
Section titled “EmailJob and EmailBatch”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.
| Handle | Poll method | Returns |
|---|---|---|
EmailJob | status(), 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 |
EmailBatch | results(*, timeout=900.0, poll_interval=5.0, on_poll=None), which polls and returns in one call | EmailBatchResults |
Both take the same three keyword arguments:
| Parameter | Type | Default | Description |
|---|---|---|---|
timeout | float | 900.0 | Seconds before JobTimeoutError is raised |
poll_interval | float | 5.0 | Seconds slept between polls |
on_poll | Callable[[...], None] | None | None | Called 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.
Exceptions
Section titled “Exceptions”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| Exception | Base | Raised when | Attributes |
|---|---|---|---|
DiscolikeError | Exception | Any 4xx status not mapped to a more specific class | status_code: int | None, payload: Any |
AuthenticationError | DiscolikeError | HTTP 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 |
PlanAccessError | DiscolikeError | HTTP 402 or 403 | inherited |
ValidationError | DiscolikeError | HTTP 400 or 422 | inherited |
NotFoundError | DiscolikeError | HTTP 404 | inherited |
ServerError | DiscolikeError | Any status ≥ 500, after retries are exhausted | inherited |
RateLimitError | DiscolikeError | HTTP 429, after retries are exhausted | inherited, plus retry_after: float | None |
APIConnectionError | DiscolikeError | A transport-level failure that is not retryable, or retries exhausted. status_code and payload are always None | inherited |
JobFailedError | DiscolikeError | A job reaches status == "failed", or an email find job completes without a result | inherited; payload is the job status dict |
JobTimeoutError | DiscolikeError | The client-side wait deadline elapses. The task keeps running server-side | inherited |
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:
| Body | Message |
|---|---|
{"detail": "..."} | that string |
{"detail": [{"loc": [...], "msg": "..."}]} | "loc.joined: msg" parts joined with "; " |
| Any other JSON | json.dumps(payload) truncated to 500 characters |
| Non-JSON | response.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:
| Exception | Raised when |
|---|---|
pydantic.ValidationError | Constructing 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 |
ValueError | append() 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 50from 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.