# DiscoLike API > REST API and MCP server for B2B company discovery, firmographic data, and contact enrichment across 80M+ business domains. Base URL: https://api.discolike.com/v1 Authentication: x-discolike-key header, or Authorization: Bearer with an OAuth access token OpenAPI spec: https://api.discolike.com/v1/openapi.json MCP server: https://api.discolike.com/v1/mcp (OAuth) Concise reference: https://docs.discolike.com/llms.txt Supported values for the state filter: https://docs.discolike.com/states/ --- # Overview ## DiscoLike API Documentation The DiscoLike API provides programmatic access to a B2B company discovery and data enrichment platform covering 80M+ business domains across 180+ countries. Search by domain lookalike, natural language ideal customer profile (ICP) descriptions, or exact phrase matching. All API endpoints use REST conventions with JSON responses. Authenticate with an API key in the `x-discolike-key` header, or with an OAuth bearer token, which is what the [Python SDK](https://docs.discolike.com/sdk/) and [CLI](https://docs.discolike.com/cli/) use after `discolike auth login`. See [pricing](https://discolike.com/pricing/) for plan details. ## Getting Started 1. **[Create an API key](https://app.discolike.com/account/management/keys)**, which requires a DiscoLike account ([sign up](https://discolike.com/signup), or [let your agent do it](https://docs.discolike.com/guides/agent-signup/)); or skip the key and run `discolike auth login` from the [CLI](https://docs.discolike.com/cli/) 2. **[Authenticate requests](https://docs.discolike.com/api/access/)** using the `x-discolike-key` header or an OAuth bearer token 3. **[Run your first discovery](https://docs.discolike.com/api/endpoints/discover/)** to find companies matching your criteria 4. **[Enrich results](https://docs.discolike.com/api/endpoints/bizdata/)** with firmographic profiles, contacts, and growth data ## Core Workflows | Workflow | Endpoints | Description | |----------|-----------|-------------| | **Find companies** | [Discover](https://docs.discolike.com/api/endpoints/discover/) → [Count](https://docs.discolike.com/api/endpoints/count/) → [Segment](https://docs.discolike.com/api/endpoints/segment/) | Search by lookalike domains, ICP text, or filters. Preview counts before full queries. Group results into segments. | | **Enrich domains** | [BizData](https://docs.discolike.com/api/endpoints/bizdata/), [Score](https://docs.discolike.com/api/endpoints/score/), [Growth](https://docs.discolike.com/api/endpoints/growth/) | Get firmographic profiles, domain scores, and quarterly growth metrics for any domain. | | **Find contacts** | [Contacts](https://docs.discolike.com/api/endpoints/contacts/), [Contact Match](https://docs.discolike.com/api/endpoints/contact-match/), [Contact Bulk Match](https://docs.discolike.com/api/endpoints/contact-bulk-match/) | Search B2B contacts by persona similarity, domain, or name. Batch-match lists of people to profiles. | | **Match & enrich lists** | [Match](https://docs.discolike.com/api/endpoints/match/), [Append](https://docs.discolike.com/api/endpoints/append/) | Resolve company names to domains. Bulk-append firmographic data to prospect lists. | | **Map relationships** | [Vendors](https://docs.discolike.com/api/endpoints/vendors/), [Subsidiaries](https://docs.discolike.com/api/endpoints/subsidiaries/), [PublicLink](https://docs.discolike.com/api/endpoints/publiclink/) | Detect technology stacks, explore corporate hierarchies, and find related domains. | ## AI Assistant Integration Connect AI coding assistants directly to DiscoLike data via the **[MCP server](https://docs.discolike.com/mcp/)**. Works with Claude Code, Codex, Claude Desktop, Cursor, VS Code, Windsurf, and Zed. One command to set up, OAuth to authenticate. For agent guidance as well as tool access, use the [DiscoLike agent skill package](https://github.com/Discolike/discolike-skills). It ships native adapters for Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, Grok, and agents that read `AGENTS.md`. The skill tells an agent when to use DiscoLike; connect the MCP server separately to make the tools available. See the **[MCP tools reference](https://docs.discolike.com/mcp/tools/)** for all available tools. ## Datasets Monthly, weekly, or daily data feeds available as JSON or delimited files: - [BizData](https://docs.discolike.com/datasets/bizdata/): Company profiles with firmographic data - [Domain Status](https://docs.discolike.com/datasets/domain-status/): Active/inactive domain tracking - [Vendor Integration](https://docs.discolike.com/datasets/vendor-integration/): Technology stack data - [Subsidiaries](https://docs.discolike.com/datasets/subsidiaries-linkage/): Corporate hierarchy data - [Site Text](https://docs.discolike.com/datasets/site-text/): Extracted homepage content - [DNS](https://docs.discolike.com/datasets/dns/): DNS record data - [Certificate Stream](https://docs.discolike.com/datasets/certificate-stream/): SSL certificate history - [Redirect Domains](https://docs.discolike.com/datasets/redirect-domains/): Domain redirect mapping - [PublicLink](https://docs.discolike.com/datasets/publiclink/): Shared contact information links --- # MCP Integration ## DiscoLike MCP Server Setup Connect AI assistants like Claude Code, Codex, Claude Desktop, Cursor, VS Code, Windsurf, and Zed directly to DiscoLike company profiles, contacts, and enrichment data. **Server URL:** `https://api.discolike.com/v1/mcp` ## Setup ### Claude Code Copy the prompt below and paste it into a Claude Code session. Claude Code registers the server itself, then hands you a one-click browser sign-in. No API key. ```text Set up the DiscoLike MCP server in Claude Code for me. DiscoLike connects by browser sign-in (OAuth), never by an API key, so don't ask me for one and don't add any --header flag. 1. Register it as a streamable-HTTP MCP server named "discolike" at user scope so it is available in every project: claude mcp add --transport http --scope user discolike https://api.discolike.com/v1/mcp If a discolike server already exists, run `claude mcp remove discolike --scope user` first so it is not duplicated. 2. Tell me to run /mcp, select discolike, and choose Authenticate. A browser tab opens on discolike.com; I log in (Google or Microsoft sign-in works) and click Authorize. If I don't have an account yet, create one for me with POST https://api.discolike.com/v1/public/signup (see https://docs.discolike.com/guides/agent-signup/), wait for me to confirm the email, then have me authenticate again. 3. Confirm it worked before calling it done: the discolike tools should be listed under /mcp, or run a query like "how many companies in the US do managed IT services". ``` **What you will see** After the prompt runs, `/mcp` shows discolike with a "needs authentication" status. Choose **Authenticate**; your browser opens the authorization flow. Click **Authorize** to grant Claude Code access to your DiscoLike account. Prefer to run it yourself? The one command is: ```shell claude mcp add --transport http --scope user discolike https://api.discolike.com/v1/mcp ``` ### Codex Copy the prompt below and paste it into a Codex chat. Codex configures the server itself, then hands you a browser sign-in. No API key. ```text Set up the DiscoLike MCP server for me in Codex. DiscoLike connects by browser sign-in (OAuth), never by an API key, so don't ask me for one and don't set bearer_token_env_var. 1. Add a streamable-HTTP MCP server named discolike with: codex mcp add discolike --url https://api.discolike.com/v1/mcp --oauth-resource https://api.discolike.com/v1/mcp If a discolike entry already exists in ~/.codex/config.toml (%USERPROFILE%\.codex\config.toml on Windows), update it in place instead of duplicating it. 2. Tell me to fully quit and reopen Codex so it picks up the new config. 3. Get me signed in. In the desktop app or IDE extension, tell me to open the MCP servers list and press Authenticate next to discolike. In the CLI, run `codex mcp login discolike` yourself. A browser tab opens on discolike.com; I log in (Google or Microsoft sign-in works) and click Authorize. If I don't have an account yet, create one for me with POST https://api.discolike.com/v1/public/signup (see https://docs.discolike.com/guides/agent-signup/), wait for me to confirm the email, then have me sign in again. 4. Confirm it worked before calling it done: /mcp should list discolike and its tools, or run a query like "how many companies in the US do managed IT services". ``` Prefer to run it yourself? The one command is: ```shell codex mcp add discolike --url https://api.discolike.com/v1/mcp --oauth-resource https://api.discolike.com/v1/mcp ``` Codex opens your browser to authorize. Sign in to DiscoLike and click **Authorize**. ### Claude Desktop/Cowork **1. Add the connector** Go to **Claude Desktop → Settings → Connectors → Add Custom Connector**. In the "Add Custom Connector" window: - **Name:** DiscoLike - **MCP Server URL:** `https://api.discolike.com/v1/mcp` Click **Add**. **2. Connect** DiscoLike will appear in the connectors list. Click **Connect** next to it. **3. Authorize in your browser** A browser window will open. Log in to DiscoLike and click **Authorize**. You will be redirected back to Claude Desktop. ### Cursor **1. Create the config file** Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json { "mcpServers": { "discolike": { "url": "https://api.discolike.com/v1/mcp" } } } ``` **2. Open Cursor Settings → Tools** You'll see DiscoLike appear under "Installed MCP Servers" with a "Needs authentication" status. **3. Click "Connect" to authenticate** Cursor will prompt you to open the authorization URL. **4. Authorize in your browser** Click "Authorize" to grant Cursor access to your DiscoLike account. **5. Return to Cursor** Your browser will ask to open the callback link in Cursor. **6. Connected!** DiscoLike will show as connected with all tools available. **7. Start using DiscoLike tools** Ask Cursor to use any DiscoLike tool in your conversations. ### VS Code Add to `.vscode/mcp.json`: ```json { "servers": { "discolike": { "type": "http", "url": "https://api.discolike.com/v1/mcp" } } } ``` VS Code uses `servers` rather than `mcpServers`, each entry needs `"type": "http"`, and browser sign-in needs VS Code 1.101 or newer. After saving, VS Code prompts you to trust the server, then to sign in. ### Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json { "mcpServers": { "discolike": { "serverUrl": "https://api.discolike.com/v1/mcp" } } } ``` Windsurf's remote-server field is `serverUrl`, not `url`. After saving, open the MCP panel in Cascade and click **Login** next to discolike. ### Zed Add to `~/.config/zed/settings.json`: ```json { "context_servers": { "discolike": { "url": "https://api.discolike.com/v1/mcp" } } } ``` Save and Zed prompts you to authenticate; log in to DiscoLike in the browser and you are connected. ## Agent skill package The [DiscoLike agent skill package](https://github.com/Discolike/discolike-skills) teaches an agent when to reach for DiscoLike, which access mode to pick, how to search well, and how to open an account without a browser. It has native adapters for Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, Grok, and agents that read `AGENTS.md`. The skill is guidance, not a replacement for this MCP connection: configure the server above before expecting the agent to call DiscoLike tools. For Claude Code, install it from the marketplace: ```text /plugin marketplace add Discolike/discolike-skills /plugin install discolike@discolike ``` For every other client, follow the installation table in the [skill package README](https://github.com/Discolike/discolike-skills#codex-cursor-and-other-agents). Every adapter uses the same canonical `SKILL.md`, so the workflow and its paid-operation safeguards stay consistent across clients. The MCP server also serves the same skills as resources, so a client that reads MCP resources can load them without installing anything: `skill://discolike/SKILL.md`, `skill://setup/SKILL.md` and `skill://feedback/SKILL.md`, with each skill's supporting files under `skill:///` (for example `skill://discolike/flows.md`) and a file list at `skill:///_manifest`. ## DiscoLike MCP Tools Reference ## Managed Prospecting The [managed prospecting API](https://docs.discolike.com/api/endpoints/prospecting/) documents the shared brief, limits, billing, and result contract. | Tool | Parameters | Description | | --- | --- | --- | | `start-prospecting` | `brief`, `idempotency_key` | Start and automatically approve a durable run; reuse the same key for retries. Pass `brief.customer_domains` to find lookalikes of your customers, segment by segment. When the brief names no company count, defaults to 100 target companies (versus 1000 for REST/SDK) since approval, and the company and contact research spend it triggers, happens immediately. Email lookups wait for the person's answer at the contacts review. | | `get-prospecting-run` | `run_id`, `offset=0`, `limit=100`, `events_after=0` | Poll every five seconds; `limit` accepts 1–100; partial results are paginated. Inspect `stop_reason`, even when status is `completed`. A `needs_input` run with a `question` waits on the person (see below); `needs_input` with no `question` is final. | | `cancel-prospecting` | `run_id` | Stop scheduling work; incurred charges and completed results remain. | | `answer-prospecting` | `run_id`, `question_seq`, `text`, `idempotency_key` | Send the person's answer to the run's open `question`. Ask the person and send what they say; never answer for them. When `question.replies` is not empty, send one of its labels verbatim as `text` (1–4,000 characters). Pass `question.question_seq` as given, `null` included; a stale or missing `question_seq` returns an error, so poll again for the current question. Reuse the same key for retries. | MCP start automatically approves; the REST approval, list, rename, and delete operations are not additional MCP tools, and `answer-prospecting` covers answering a paused run. MCP responses omit the REST chat fields. MCP runs decide the pilot, tail quality and top-up checkpoints on their own. They pause at `needs_input` only when the person has to answer, and `get-prospecting-run` then returns a `question` object: | Field | Description | | --- | --- | | `question_seq` | The question message's `seq`, or `null` for a stop that posted no question message. Pass it to `answer-prospecting` as given. | | `reason` | The run's `stop_reason`. | | `content` | The question or stop text to show the person. | | `replies` | Exact reply labels; send one verbatim. Empty when the question takes free text. | | `estimate` | For a `contacts_review`, the email estimate: `checked`, `fit`, `matched`, `companies`, `lookups`, `find_rate`, `basis`, `expected_emails`, `minutes`, `records`, `early_lookups`, `early_found`, and `early_running`. `null` otherwise. | - **`contacts_review`**: the wave's companies are checked and their contacts found and qualified, and no email has been looked up yet. Show the person `content` and `estimate`, then send "Find emails", "Find more companies first" (offered only when more candidates exist) or "Finish". Email lookups reserve records of the plan quota until each settles and bill only for emails found; the question shows a USD figure when the run has a spending limit. - **`provider_unavailable`, `stage_unavailable`, `account_unavailable`**: an outage or account problem stopped the run. After the person fixes it, send "Retry"; "Cancel" stops the run. - **`question`**: the run needs missing input. `replies` is empty; send the person's own words. `question` is `null` while an answer is still being read, so poll after answering instead of answering again. A `needs_input` run with no `question` (for example `run_timeout` or `action_uncertain`) is final. Answering returns an error while the account has no quota left, until it is refilled; `cancel-prospecting` still works. The client's assistant subscription does not supply backend model credentials. Intent interpretation and wizards use platform credentials; coordination and independent contact qualification use customer LLM integrations. Native contact extraction still needs a generative LLM for qualification, unless company checks also run on DiscoLike VibeCheck: a run on DiscoLike Groove and VibeCheck, or on VibeCheck with indexed contacts only or no contacts, uses no customer LLM and keeps contacts by title without persona assessment. Existing provider and processing charges apply. Otherwise, indexed and native generated candidates must satisfy persona assessment; similarity and upstream title labels alone do not establish acceptance. ## Discovery & Company Data | Tool | Description | |------|-------------| | account-status | Check subscription plan and limits | | usage-statistics | View API usage for billing period | | discover-similar-companies | Find companies matching examples, description, or natural language ICP prompt | | count-matching-domains | Count matches before full search | | list-industry-categories | Look up industry category values and their definitions (full taxonomy, by name, or by search term) | | business-profile | Get firmographic data | | digital-footprint-score | Get score with breakdown | | growth-metrics | View quarterly trends | | segment-domains | Group companies into clusters | | match-company-to-domain | Find domain for company name | | bulk-match-company-to-domain | Batch match up to 1000 company names to domains with full bizdata appended | | extract-website-text | Get cached text content from pre-crawled websites | | domain-redirects | Get redirect mappings for a domain | | append-data | Bulk-enrich domains with firmographics, growth, redirects, or domain status | | vendor-and-technology-data | See technologies a company uses | | subsidiaries-and-parent-companies | Explore corporate hierarchies | | public-company-links | Find related domains sharing contact info | ## Contacts | Tool | Description | |------|-------------| | search-contacts | Search for business contacts. Response is keyed by company domain; each entry contains firmographic fields plus nested `contacts[]`. | | count-contacts | Count contacts matching filters before running a full search | | contact-lookup | Look up a specific contact by persona ID, LinkedIn profile, or email | | match-contacts | Match contacts by name/company via text search (lightweight, no usage cost) | | bulk-match-contacts | Batch match a list of names, emails or LinkedIn profile URLs to contacts with optional full-data enrichment; LinkedIn URLs, then emails, are exact-matched first | | generate-contacts | Discover candidate contacts at given domains using your own search provider (BYOS) plus either your own LLM (BYOK) or DiscoLike Groove (`discolike/groove`), the native extractor. Pass `integration_id` as an LLM integration UUID, or `native` for keyless extraction with no title validation against `icp_text`; omit it for native when it is available and a search provider resolves, otherwise your default LLM integration. Web search always enabled. Returns a task ID; poll `get-research-status` for per-domain contact rows plus the company's inferred email pattern, and for `title_validation` (`llm` or `none`). `email` is null unless a source shows it; pass `find_emails=true` to run the email finder over named, email-less rows before the task completes (fills `email` and `email_status`; found addresses bill under the finder's rules). Otherwise no platform billing; results are candidate data, so validate before use. | | find-emails | Find likely email addresses for up to 500 people (first name, last name, domain) by enumerating and SMTP-verifying pattern candidates per domain. Returns a `batch_id` immediately; poll `get-email-results` until every job is terminal. Only proven addresses (status `found`) bill as records; catch-all and pattern-guess results are free. | | get-email-results | Poll a find-emails batch by `batch_id`. Returns total/completed/failed counts plus per-job results (matched email + pattern). Keep polling until every job's status is `completed` or `failed`. | ## Saved Queries & Lists | Tool | Description | |------|-------------| | list-saved-queries | List saved queries with their domains for use in inclusion/exclusion | | save-mcp-query | Save MCP results as a reusable query | | save-exclusion-list | Create an exclusion list from domains and/or persona IDs | | list-tags | List all unique tags in your organization | | add-tags-to-query | Add tags to a saved query for organization | | remove-tags-from-query | Remove tags from a saved query | | get-domains-by-tags | Get all domains from queries matching specified tags | ## Enrichment & Research | Tool | Description | |------|-------------| | research-companies | Run a DiscoGen research prompt against each domain in a list | | research-contacts | Run an LLM prompt against contacts, same as above but for people | | get-research-status | Poll async DiscoGen task status and results | | cancel-research | Cancel a running DiscoGen task | | list-research-providers | Check configured LLM and search providers before running DiscoGen. The LLM list also carries a `native` row, named "DiscoLike Groove", for DiscoLike's keyless contact extractor, usable only by generate-contacts | | validate-icp-fit | Validate whether a list of companies match your ICP description; `integration_id: "native-icp"` scores on DiscoLike's own model at no LLM cost | ## CRM Integrations Sync with HubSpot, Pipedrive, or Salesforce. Connect the CRM from app **Settings > Integrations** first. These tools are in **beta**: flows are live end-to-end, but expect rough edges while hardening completes. Full parameters and workflows: [MCP CRM Tools](https://docs.discolike.com/mcp/crm/). | Tool | Description | |------|-------------| | push-to-crm | Push a saved query's companies or contacts into the connected CRM (Salesforce: pick Leads or Accounts via `sf_target_object`) | | crm-suppress | Snapshot the CRM's companies or contacts into an exclusion list so existing pipeline stops surfacing in discovery | | crm-import-companies | Import the CRM's companies as tagged DiscoLike domain lists | | crm-match-companies | Run domain-less CRM records through company matching | | crm-writeback-domains | Write matched domains back onto CRM records by id | | crm-match-contacts | Run email-less CRM people through contact matching with enrichment | | crm-writeback-emails | Write matched emails back onto CRM records (keyed on the match task id) | | crm-enrich-contacts | Find contacts for the CRM's companies and push them in (billed net-new for delivered contacts only) | | crm-writeback-segments | Write segment labels onto CRM records by domain | | crm-audit-companies | Check each CRM company's operating status (active / closed / redirected), tag the records, and save closed companies as a list | ## Example Conversation 1. *"Find US-based SaaS companies with 50-200 employees that recently raised Series A"* 2. AI calls `discover-similar-companies` with `icp_prompt`, which automatically extracts filters (country: US, employee_range, category: SAAS), generates ICP text, suggests domains, and returns results in one call 3. *"Segment these into market categories"* ## MCP CRM Tools Ten MCP tools that move data between DiscoLike and your CRM. Flows are live end-to-end, but expect rough edges while hardening completes. Supported providers: `hubspot`, `pipedrive`, `salesforce`. ## Before You Start Connect the CRM once from the app under **Settings > Integrations**. Every tool below refuses to run without an active connection, and returns a re-authorization error if the stored token has expired. Reconnect from the same screen. Connection status is also readable over REST via `GET /v1/crm/connections`; see the [CRM API](https://docs.discolike.com/api/endpoints/crm/). ## Tools | Tool | Parameters | What it does | |------|-----------|--------------| | push-to-crm | `query_id`, `provider`, `mode`, `sf_target_object` | Push a saved query's companies or contacts into the CRM | | crm-suppress | `provider`, `kind` | Snapshot the CRM into an exclusion list | | crm-import-companies | `provider` | Import the CRM's companies as tagged DiscoLike domain lists | | crm-match-companies | `provider`, `min_match_confidence` | Find domains for domain-less CRM companies | | crm-writeback-domains | `provider`, `query_id`, `min_confidence` | Write matched domains back onto CRM records | | crm-match-contacts | `provider` | Find emails for email-less CRM people | | crm-writeback-emails | `provider`, `match_task_id`, `min_confidence` | Write matched emails back onto CRM records | | crm-enrich-contacts | `provider`, `icp_text`, `contacts_per_company`, `mode`, `sf_target_object` | Find contacts at the CRM's companies and push them in | | crm-writeback-segments | `provider`, `query_id` | Write segment labels onto CRM records | | crm-audit-companies | `provider` | Tag each CRM company with its operating status | Every tool runs as a background job. The ones that return a `task_id` are asynchronous; results appear via `list-saved-queries` or directly on the CRM records as the job progresses. ### Shared parameters | Parameter | Values | Notes | |-----------|--------|-------| | provider | `hubspot`, `pipedrive`, `salesforce` | Required on every tool | | mode | `push` (default), `enrich` | `push` creates records that don't exist yet and updates the ones that do. `enrich` only updates existing records; it never creates any. | | sf_target_object | `leads` (default), `accounts` | Salesforce only. Other providers ignore it. | | min_confidence | 50–100, default 70 | Write-back floor: only matches at or above this confidence land on the CRM. | | min_match_confidence | 50–100, default 50 | Match floor on `crm-match-companies`. 50 is the standard quality floor. | ## Pushing DiscoLike Data Out **push-to-crm** takes a saved query and syncs its results into the CRM. On Salesforce, `sf_target_object` decides whether companies land as Leads or Accounts. > *"Push my Series A SaaS list to HubSpot"* **crm-enrich-contacts** goes the other direction from a company list: it reads the CRM's companies by website domain, searches the DiscoLike contact index for people at those companies, and pushes the email-bearing matches in as contacts, auto-linked to their company (Salesforce Account, Pipedrive organization, HubSpot domain association). Describe who to find in `icp_text` as free text ("VP of Engineering or CTO"); `contacts_per_company` accepts 1–10 and defaults to 3. Covers up to 1,000 companies per run. Billed per net-new contact delivered. > *"Find decision makers for my Salesforce accounts"* ## Keeping Existing Pipeline Out of Results **crm-suppress** snapshots the CRM into an exclusion list, which then shows up in `list-saved-queries`. Pass its ID as `exclusion_query_id` to `discover-similar-companies` or `search-contacts`. `kind="companies"` excludes by domain. `kind="contacts"` resolves the CRM's contact emails against the DiscoLike contact index. CRM contacts whose email DiscoLike has never seen are skipped, since they can't appear in results anyway. > *"Don't show me companies I already have in HubSpot"* ## Filling In Missing CRM Data Both fill-in flows are two-step: match first, review, then write back. **Domains for domain-less companies:** 1. `crm-match-companies` pulls the CRM's companies without a website and runs them through company matching. Results save as a query named ` companies from `. 2. `crm-writeback-domains` takes that `query_id` and writes each matched domain onto its CRM record, skipping anything below `min_confidence`. **Emails for email-less people:** 1. `crm-match-contacts` pulls the CRM's people lacking an email and matches them by name and company. Matched contacts are enriched with their email, billed per net-new contact. Returns a `task_id`. 2. `crm-writeback-emails` takes that `match_task_id` and writes the emails back. Called while the match is still running, it rejects rather than writing partial results. > *"Find websites for my domain-less Salesforce accounts, then update the records"* ## Enriching CRM Records **crm-import-companies** exports every company with a website domain from the CRM and saves them as DiscoLike domain lists, 10,000 domains per list, tagged `crm:`. Use those lists as the input to any other DiscoLike operation. **crm-writeback-segments** runs after `segment-domains` over CRM companies. Pass the segmentation query's ID (it equals the segment task ID) and each company's segment label lands on the CRM record matched by domain: a DiscoLike Segment property on HubSpot and Pipedrive, and a `DiscoLike segment:` line inside Description on Salesforce. Unclustered companies are skipped. **crm-audit-companies** checks every CRM company's website against the DiscoLike index (including domains no longer actively indexed) and writes an operating status onto each record: `active`, `closed`, `redirected to `, or `unknown`. On HubSpot and Pipedrive the status lands in a DiscoLike Domain Status property, so you can filter on it and bulk-archive dead accounts natively; on Salesforce it's a `DiscoLike status:` line in Description. Closed companies are also saved as a reviewable domain list. Covers up to 10,000 companies per run. Billed per net-new domain. > *"Which of my HubSpot accounts are dead?"* ## Plan Requirements | Tool | Requires | |------|----------| | crm-match-contacts, crm-writeback-emails, crm-enrich-contacts | Contacts access | | All others | Saved queries access | --- # Guides ## Self-Hosting: Run Your Own LLM with DiscoGen Run your own LLM for DiscoGen on your hardware. This guide sets up a **proof-of-concept** inference stack on a single GPU using Docker: [Ollama](https://github.com/ollama/ollama) for inference, [LiteLLM](https://github.com/BerriAI/litellm) as an API proxy, and [Cloudflared](https://github.com/cloudflare/cloudflared) for tunnel access. ## Why self-host? | | Cloud LLM (OpenAI, Anthropic) | Self-hosted | |---|---|---| | **Cost** | Pay per token. Scales linearly with volume. | Fixed GPU cost. Process unlimited tokens for a flat hourly rate. | | **Data privacy** | Prompts and domain data sent to third-party APIs | Everything stays on your network | | **Latency** | Depends on provider load and rate limits | Dedicated GPU, no queue, no throttling | | **Model control** | Limited to provider's model catalog | Run any open-source model, swap anytime | ## Prerequisites - **GPU:** NVIDIA with 96GB+ VRAM recommended (16GB minimum for smaller models). Don't have one? Rent on-demand from [Lambda](https://lambda.ai/) (used in this guide), [RunPod](https://www.runpod.io/), [Vast.ai](https://vast.ai/), or [CoreWeave](https://www.coreweave.com/). Most providers come with NVIDIA drivers pre-installed. - **Docker** with [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). The setup script installs both if missing. ### Pick a model The setup script detects your VRAM and offers these defaults: | VRAM | Model | | |------|-------|---| | 80GB+ | **Llama 4 Scout** | MoE, 17B active params. Best single-GPU option for structured output | | 40-80GB | **Llama 3.1 70B** | Dense 70B. Reliable JSON generation, well-tested | | 20-40GB | **Qwen3 32B** | Comparable to 72B models at half the VRAM | | 8-20GB | **Llama 3.1 8B** | Lightweight. Good for testing the pipeline | Or select **Custom model** to enter any model name from [ollama.com/library](https://ollama.com/library). ## Setting up ```bash mkdir discolike-self-hosting && cd discolike-self-hosting curl -LO https://docs.discolike.com/self-hosting/docker-compose.yml curl -LO https://docs.discolike.com/self-hosting/setup.sh chmod +x setup.sh && ./setup.sh ``` The script detects your GPU and lets you pick a model that fits: It then generates API keys, starts the Docker stack, pulls the model (this can take a while depending on your connection), and verifies inference end-to-end: When complete, you get your tunnel URL, model name, and API key: ## Connecting to DiscoLike ### Adding your model 1. Go to **Settings → Integrations → AI Providers → Add** 2. Fill in the form: | Field | Value | |-------|-------| | Provider | **Bring Your Own Model** | | Integration Name | e.g. **`llama4_scout`** | | Base URL | Your tunnel URL, **without** the `/v1` suffix | | Model Name | **`openai/llama4:scout`** | | API Key | The LiteLLM key from setup | 3. Click **Save**. DiscoLike sends a test prompt to validate the connection. This is the most common setup issue. DiscoLike uses LiteLLM to route requests, and the `openai/` prefix tells it to use the OpenAI-compatible protocol your LiteLLM proxy speaks. Without it, validation will fail. **`openai/llama4:scout`** ✓    `llama4:scout` ✗ Same for other models: **`openai/llama3.1:70b`**, **`openai/qwen3:32b`**, etc. ### Adding web search (optional) Serper gives DiscoGen access to live web results alongside your domain data. Useful for enriching companies with recent news, funding rounds, or hiring signals. 1. Grab an API key from [serper.dev](https://serper.dev). A paid plan is recommended as the free tier has strict rate limits that will slow down DiscoGen batch processing. 2. Go to **Settings → Integrations → Search Providers → Add** | Field | Value | |-------|-------| | Provider | **Serper** | | Integration Name | e.g. **`serper`** | | API Key | Your Serper key | | Search Model | **`serper/search`** (auto-populated) | 3. Click **Save**. DiscoLike validates with a test search. ### Running DiscoGen Open **DiscoGen** and select your self-hosted model from the **Model** dropdown. If you added Serper, toggle **Enable web search**, pick a **Search Depth**, and select your provider under **Search Source**. Hit **Submit**. DiscoGen sends each domain through your model and streams results into the table. ## Setting up a named tunnel For a persistent subdomain that survives restarts, replace the quick tunnel with a named one: ```bash cloudflared tunnel login cloudflared tunnel create discolike-llm cloudflared tunnel route dns discolike-llm llm.yourdomain.com cloudflared tunnel run --url http://localhost:4000 discolike-llm ``` Then update your **Base URL** in the DiscoLike integration to `https://llm.yourdomain.com`. ## Managing your stack ```bash docker compose up -d # Start everything docker compose down # Stop everything docker compose logs -f # Follow logs docker compose logs cloudflared # Get current tunnel URL docker exec ollama ollama list # List installed models docker exec ollama ollama pull qwen3:32b # Pull another model ``` Switching models? Edit `litellm-config.yaml`, run `docker compose restart litellm`, and update the **Model Name** in DiscoLike (with the `openai/` prefix). ## Troubleshooting | Symptom | Fix | |---------|-----| | Validation fails | Model name must start with **`openai/`**. Base URL must **not** end with `/v1` | | Model won't load | `nvidia-smi`. If VRAM is full, re-run `./setup.sh` and pick a smaller model | | LiteLLM returns 500 | `docker compose logs litellm --tail 50`. Usually Ollama is still loading | | Tunnel unreachable | `docker compose logs cloudflared`. If the URL changed, update DiscoLike | Quick validation test from your terminal: ```bash curl https://your-tunnel-url/v1/chat/completions \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "llama4:scout", "messages": [{"role": "user", "content": "hi"}], "max_tokens": 5}' ``` If this returns a response, the stack is working. Check your DiscoLike configuration if validation still fails. ## Agent Signup: Let Your Agent Open Your DiscoLike Account Your agent can open your DiscoLike account without a browser. It sends one request to `POST https://api.discolike.com/v1/public/signup` with your work email and name. You get a confirmation email, log in at [app.discolike.com](https://app.discolike.com), pick a plan, and issue API keys or authorize the [MCP server](https://docs.discolike.com/mcp/). The agent never receives a credential. Endpoint reference: [Signup API](https://docs.discolike.com/api/endpoints/signup/). Free-mail and disposable domains are rejected; `409` means the account already exists. ## By agent ### DiscoLike CLI/SDK No prompt needed: the CLI and the Python SDK both sign up directly, and neither needs a login first. ```bash uvx --from discolike-cli discolike signup --email jane@acme.com --first-name Jane --last-name Doe ``` ```python from discolike import signup result = signup(email="jane@acme.com", first_name="Jane", last_name="Doe", agent="my-agent") print(result.next_step) ``` Full options: [`discolike signup`](https://docs.discolike.com/cli/reference/#signup) and [`signup()`](https://docs.discolike.com/sdk/reference/#signup). ### Claude Code Paste the prompt above into the Claude Code session. Claude Code runs the request with its built-in `curl` or `WebFetch` tooling and relays `next_step`. Once the account exists, connect the MCP server: [MCP setup](https://docs.discolike.com/mcp/). ### Codex Paste the prompt above. Codex executes the `curl` call in its sandbox; approve the network request if prompted. ### Claude Desktop/Cowork Paste the prompt above. Claude uses its web tools to make the request. If the environment cannot reach the network, ask it to hand you the `curl` command instead and run it yourself. ### Cursor Paste the prompt above into Agent mode. Cursor runs the `curl` in the integrated terminal; approve the command. ### ChatGPT Paste the prompt above. In agent mode ChatGPT can make the request directly. In plain chat it will produce the `curl` command for you to run: ```shell curl -X POST "https://api.discolike.com/v1/public/signup" \ -H "Content-Type: application/json" \ -d '{"email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe", "agent": "chatgpt"}' ``` ### Google ADK A plain Python function becomes an ADK tool. The endpoint is also in the [OpenAPI spec](https://api.discolike.com/v1/openapi.json) if you prefer `OpenAPIToolset`. ```python import httpx from google.adk.agents import LlmAgent SIGNUP_URL = "https://api.discolike.com/v1/public/signup" def signup_discolike(email: str, first_name: str, last_name: str) -> dict: """Create a DiscoLike account for the user. Returns next_step text to relay to them.""" response = httpx.post( SIGNUP_URL, json={"email": email, "first_name": first_name, "last_name": last_name, "agent": "google-adk"}, ) response.raise_for_status() return response.json() agent = LlmAgent( model="gemini-3.6-flash", name="discolike_onboarding", instruction="When the user wants a DiscoLike account, collect their work email and name, call signup_discolike, and relay next_step.", tools=[signup_discolike], ) ``` ### OpenAI Agents SDK ```python import httpx from agents import Agent, function_tool SIGNUP_URL = "https://api.discolike.com/v1/public/signup" @function_tool def signup_discolike(email: str, first_name: str, last_name: str) -> dict: """Create a DiscoLike account for the user. Returns next_step text to relay to them.""" response = httpx.post( SIGNUP_URL, json={"email": email, "first_name": first_name, "last_name": last_name, "agent": "openai-agents"}, ) response.raise_for_status() return response.json() agent = Agent( name="DiscoLike onboarding", instructions="Collect the user's work email and name, call signup_discolike, relay next_step.", tools=[signup_discolike], ) ``` ### LangChain ```python import httpx from langchain_core.tools import tool SIGNUP_URL = "https://api.discolike.com/v1/public/signup" @tool def signup_discolike(email: str, first_name: str, last_name: str) -> dict: """Create a DiscoLike account for the user. Returns next_step text to relay to them.""" response = httpx.post( SIGNUP_URL, json={"email": email, "first_name": first_name, "last_name": last_name, "agent": "langchain"}, ) response.raise_for_status() return response.json() ``` Bind it with `llm.bind_tools([signup_discolike])` or add it to your agent's tool list. ### CrewAI ```python import httpx from crewai.tools import tool SIGNUP_URL = "https://api.discolike.com/v1/public/signup" @tool("signup_discolike") def signup_discolike(email: str, first_name: str, last_name: str) -> dict: """Create a DiscoLike account for the user. Returns next_step text to relay to them.""" response = httpx.post( SIGNUP_URL, json={"email": email, "first_name": first_name, "last_name": last_name, "agent": "crewai"}, ) response.raise_for_status() return response.json() ``` ### n8n Add an **HTTP Request** node: - Method: `POST` - URL: `https://api.discolike.com/v1/public/signup` - Body content type: JSON - Body: `{"email": "{{ $json.email }}", "first_name": "{{ $json.first_name }}", "last_name": "{{ $json.last_name }}", "agent": "n8n"}` - No authentication. Read `next_step` from the response and forward it to the person (email, Slack, or chat node). ### Python SDK / CLI ```bash pip install discolike-cli discolike signup --email jane@acme.com --first-name Jane --last-name Doe ``` ```python from discolike import signup result = signup(email="jane@acme.com", first_name="Jane", last_name="Doe") print(result.next_step) ``` ### curl ```shell curl -X POST "https://api.discolike.com/v1/public/signup" \ -H "Content-Type: application/json" \ -d '{"email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe", "agent": "curl"}' ``` ## After signup 1. Open the confirmation email, or sign in with Google or Microsoft using the same address. 2. Accept the terms and complete your profile. 3. Pick a plan, then create an API key at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys) or authorize the [MCP server](https://docs.discolike.com/mcp/) from your agent. --- # Python SDK ## Getting Started `discolike` is the official Python client for the DiscoLike API. It wraps every REST endpoint in a typed resource namespace, handles authentication and retries, and speaks [Pydantic](https://docs.pydantic.dev/) in both directions: requests are typed models validated before anything is sent, and responses are typed models instead of raw dictionaries. Every method, request model, and response model is listed in the [Reference](https://docs.discolike.com/sdk/reference/). This page covers what you need before any of that: install, authentication, the client, and the four behaviours (request models, async, jobs, and errors) that apply across the whole surface. ## Requirements | Item | Value | |-------|-------| | Python | 3.11 or newer | | Current version | 0.3.0 | ## Install There are two packages. `discolike` is the SDK. `discolike-cli` is the command-line tool, published separately and pulled in by the `cli` extra. ### pip ```bash pip install discolike # SDK plus the `discolike` command-line tool pip install "discolike[cli]" ``` ### uv ```bash uv add discolike # SDK plus the `discolike` command-line tool uv add "discolike[cli]" ``` The CLI is documented separately in [Getting Started](https://docs.discolike.com/cli/) and [Command Reference](https://docs.discolike.com/cli/reference/). ## Authentication The client authenticates with either an API key or an OAuth session. The quickest route on a workstation is `discolike auth login` from the [CLI](https://docs.discolike.com/cli/#authentication), which logs you in through the browser and saves the session where the SDK finds it. For servers and CI, create an API key at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys) and pass it in. The client resolves a credential from these sources in order and uses the first that yields one: | Order | Source | How to set it | |-------|--------|---------------| | 1 | The `auth` argument | `Discolike(auth=OAuthCredential(...))` or `Discolike(auth=ApiKeyCredential(api_key="dk_..."))` | | 2 | The `api_key` argument | `Discolike(api_key="dk_...")` | | 3 | The `DISCOLIKE_API_KEY` environment variable | `export DISCOLIKE_API_KEY=dk_...` | | 4 | The CLI config file, holding either an OAuth session or an API key | `discolike auth login` or `discolike auth login --method api_key` | `auth` takes one of the two credential types exported from the package root, [`ApiKeyCredential` or `OAuthCredential`](https://docs.discolike.com/sdk/reference/#apikeycredential-and-oauthcredential), and wins over everything else, including a `DISCOLIKE_API_KEY` in the environment. The config file lives at `$XDG_CONFIG_HOME/discolike/config.json`, defaulting to `~/.config/discolike/config.json`. It is what `discolike auth login` writes, and it is created with mode `0600`. It has one of two shapes, selected by `auth_method`: ```json {"auth_method": "api_key", "api_key": "dk_..."} ``` ```json { "auth_method": "oauth", "oauth": { "access_token": "eyJ...", "refresh_token": "...", "expires_at": 1756426800.0, "client_id": "...", "token_endpoint": "https://auth.discolike.com/oauth/2.1/token" }, "oauth_client": { "client_id": "...", "redirect_uri": "http://127.0.0.1:51234/callback", "issuer": "https://auth.discolike.com/oauth/2.1" } } ``` `oauth_client` is the CLI's own OAuth client registration; the SDK ignores it. A missing, unreadable, or non-JSON config file is treated as empty rather than as an error, and so is an `oauth` section with a missing or malformed field. ### OAuth sessions An `OAuthCredential` sends `Authorization: Bearer ` instead of the `X-discolike-key` header. Access tokens are short-lived, so the client keeps the session alive on its own: - When the access token is within 60 seconds of `expires_at`, the client refreshes it before sending the request. - If a request comes back 401 anyway, the client refreshes once and replays that request once. A second 401 is raised as `AuthenticationError` like any other. - A refresh rotates both tokens, and the new pair replaces the credential in memory. When the credential came from the config file, the pair is also written back there, so the next process starts from the rotated tokens; before refreshing, the client re-reads the file and adopts a newer session if another process has already rotated it, rather than refreshing with a stale refresh token. - A credential passed as `auth=` is never written to disk. Refreshed tokens stay in memory, and your `OAuthCredential` instance is left untouched. A refresh the authorization server rejects, typically because the refresh token itself has expired or been revoked, raises `AuthenticationError` with the message ``OAuth session expired; run `discolike auth login` `` from the call that triggered it. If no source yields a credential, the constructor raises `AuthenticationError` **at construction time**, before any request is sent: ```python >>> from discolike import Discolike >>> Discolike() discolike._exceptions.AuthenticationError: No API key found. Set the DISCOLIKE_API_KEY environment variable, pass api_key=..., or run `discolike auth login`. Create a key at https://app.discolike.com/account/management/keys ``` That construction-time `AuthenticationError` carries `status_code=None`, since no HTTP response was involved. A key or token the server rejects produces an `AuthenticationError` with `status_code=401` on the first request instead. Key creation, rotation, and the header and bearer-token details are covered in [Authentication](https://docs.discolike.com/api/access/). ## The client `Discolike` and `AsyncDiscolike` are the only two entry points. Both take the same keyword-only arguments, and both are cheap to construct: the constructor resolves your credential and builds an HTTP client, but sends no request. ```python from discolike import AsyncDiscolike, Discolike Discolike( *, api_key: str | None = None, auth: ApiKeyCredential | OAuthCredential | None = None, base_url: str = "https://api.discolike.com/v1", timeout: float = 60.0, max_retries: int = 3, ) AsyncDiscolike(...) # identical keywords ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `api_key` | `str \| None` | `None` | Your DiscoLike API key. When `None`, the credential is resolved from the environment or the CLI config file. | | `auth` | `ApiKeyCredential \| OAuthCredential \| None` | `None` | An explicit credential. Takes precedence over `api_key`, the environment, and the config file. An `OAuthCredential` given here is refreshed in memory only and never written to disk. | | `base_url` | `str` | `"https://api.discolike.com/v1"` | Root URL every request is joined onto. It must include the API version segment. Change it to point at a staging or self-hosted deployment. | | `timeout` | `float` | `60.0` | Per-request timeout in seconds, applied to the connect, read, write, and pool phases alike. It covers each individual HTTP attempt rather than the total across retries. | | `max_retries` | `int` | `3` | Number of retries after the first attempt, so the default allows 4 total attempts. `0` disables retrying. | These five arguments are the entire configuration surface. They are set once per client and apply to every call made through it. To override the timeout for individual calls without a second client, use `with_options`. ### Per-call timeout with `with_options` ```python client.with_options(*, timeout: float) -> Discolike ``` Returns a lightweight client view that shares this client's connection pool but applies a different timeout to every call made through it. The parent client is untouched, and closing a view never closes the shared pool; only closing the parent does. ```python from discolike.requests import AppendParams, DiscoverParams client = Discolike() result = client.with_options(timeout=300).append(AppendParams(query_id=["a1b2c3"], dataset=["bizdata"])) # one slow call client.discover(DiscoverParams(icp_prompt="B2B SaaS in fintech")) # default 60s timeout ``` ### Lifecycle Closing a client releases its connection pool. Use the context manager and you cannot forget: ### Sync ```python from discolike import Discolike with Discolike() as client: usage = client.account.usage() # equivalent client = Discolike() try: usage = client.account.usage() finally: client.close() ``` ### Async ```python import asyncio from discolike import AsyncDiscolike async def main() -> None: async with AsyncDiscolike() as client: usage = await client.account.usage() # equivalent client = AsyncDiscolike() try: usage = await client.account.usage() finally: await client.aclose() asyncio.run(main()) ``` Construct one client for the life of your process or request handler and reuse it. Building a client per call throws away connection reuse and repeats the TLS handshake every time; a client that is never closed keeps its connection pool open, and on the async side leaves the event loop with open sockets at shutdown. ## Your first call Fetch the business data profile for a single domain. With `DISCOLIKE_API_KEY` set, the client needs no arguments at all. The request is a model from `discolike.requests`, one per method: ```python from discolike import Discolike from discolike.requests import CompaniesDataParams with Discolike() as client: profile = client.companies.data(CompaniesDataParams(domain="stripe.com")) print(profile.name, "|", profile.employees, "|", profile.score) ``` ``` Stripe | 10001+ | 701 ``` Counting the market before you pull it costs nothing but a request: ```python from discolike import Discolike from discolike.requests import CountParams with Discolike() as client: total = client.count(CountParams(category=["CYBERSECURITY"], country=["DE"])) print(total.to_dict()) ``` ```json {"count": 9220} ``` Every response model subclasses `DiscolikeModel`, which is configured with `extra="allow"`. Fields the API returns that the model does not declare are kept rather than dropped: read them as ordinary attributes, through `.model_extra`, or through `.to_dict()`, which returns the full response as a plain JSON-safe mapping and always includes them. ## Request models Every method that sends parameters takes a single request model as its first positional argument. The models live in `discolike.requests`, are generated from the platform's OpenAPI spec, and validate locally: a bad enum value, an out-of-range number, or a missing required field raises `pydantic.ValidationError` before any HTTP request is made, so it never costs a round trip or a credit. ```python import pydantic from discolike.requests import MatchCompanyParams try: params = MatchCompanyParams(name="Acme", min_match_confidence=10) except pydantic.ValidationError as exc: print(exc.errors()[0]["msg"]) # Input should be greater than or equal to 50 ``` Naming follows the route. Query-parameter routes use `Params` (`CompaniesDataParams`, `MatchCompanyParams`, `ContactsSearchParams`, `DiscoverParams`, `CountParams`, `AppendParams`, `SegmentParams`); JSON-body routes use the platform's own schema names (`FindEmailRequest`, `ContactFilters`, `DiscoGenProcessRequest`, `UpdateQueryRequest`). Path parameters and file uploads are not part of the model; they stay keyword-only next to it: ```python from discolike.requests import MatchBulkParams, UpdateQueryRequest client.queries.update(UpdateQueryRequest(query_name="New name"), query_id="q3") client.match.bulk(MatchBulkParams(name_column="company"), file="crm.csv") ``` Three rules govern what reaches the wire, all inherited from [`DiscolikeRequest`](https://docs.discolike.com/sdk/reference/#discolikerequest): | Rule | Effect | |------|--------| | Unset fields are omitted | A field you never set is not sent, so the server default governs. The defaults shown in the Reference are the model's declared defaults and mirror the server's | | An explicit `None` is kept | JSON-body routes send it as `null`, which is how `LLMProviderUpdateRequest(api_key=None)` keeps the stored key. Query-string routes drop it, since a query string has no null | | Unknown fields pass through | The models are `extra="allow"`, so a platform field the SDK does not know about yet can still be sent, by keyword or via `Model.model_validate({...})` | Methods that take only a path parameter (`queries.delete`, `llm_providers.get`, `email.job`), or nothing at all (`account.usage`, `discogen.models`), have no model and keep their keyword arguments. ## Async `AsyncDiscolike` mirrors `Discolike` method for method: the same namespaces, method names, request models, and return models. Every resource class has an async twin, so the [Reference](https://docs.discolike.com/sdk/reference/) is read as async by adding `await`. ### Sync ```python from discolike import Discolike from discolike.requests import CompaniesDataParams with Discolike() as client: for domain in ("stripe.com", "adyen.com"): company = client.companies.data(CompaniesDataParams(domain=domain)) print(company.domain, company.name, company.employees) ``` ### Async ```python import asyncio from discolike import AsyncDiscolike from discolike.requests import CompaniesDataParams async def main() -> None: async with AsyncDiscolike() as client: stripe, adyen = await asyncio.gather( client.companies.data(CompaniesDataParams(domain="stripe.com")), client.companies.data(CompaniesDataParams(domain="adyen.com")), ) for company in (stripe, adyen): print(company.domain, company.name, company.employees) asyncio.run(main()) ``` Both print the same thing: ``` stripe.com Stripe 10001+ adyen.com Adyen 5001-10000 ``` These are the differences: | Sync | Async | |---|---| | `Discolike(...)` | `AsyncDiscolike(...)`, same keywords | | `client.close()` | `await client.aclose()` | | `with Discolike() as client:` | `async with AsyncDiscolike() as client:` | | `client.companies.data(...)` | `await client.companies.data(...)` | | returns `Job` | returns `AsyncJob` | | `EmailJob` / `EmailBatch` | `AsyncEmailJob` / `AsyncEmailBatch` | The rehydration helpers (`client.discogen.job()`, `client.email.job()`, and `client.email.batch()`) make no request, so they stay synchronous on the async client and are called rather than awaited. One `AsyncDiscolike` is safe to share across coroutines in a single event loop; do not share one across event loops. Retry sleeps use `asyncio.sleep`, so a retry never blocks the loop. ```python import asyncio from discolike import AsyncDiscolike from discolike.requests import CompaniesScoreParams DOMAINS = ["stripe.com", "adyen.com", "checkout.com"] MAX_CONCURRENCY = 2 async def main() -> None: limit = asyncio.Semaphore(MAX_CONCURRENCY) async with AsyncDiscolike() as client: async def score(domain: str) -> tuple[str, int | None]: async with limit: result = await client.companies.score(CompaniesScoreParams(domain=domain)) return domain, result.score for domain, value in await asyncio.gather(*(score(d) for d in DOMAINS)): print(domain, value) asyncio.run(main()) ``` ``` stripe.com 701 adyen.com 458 checkout.com 641 ``` ## Jobs and polling Operations that run longer than a single HTTP request return a job handle instead of a result. Eight methods return a `Job` on `Discolike` and an `AsyncJob` on `AsyncDiscolike`: | Method | Task family | |---|---| | `client.match.bulk()` | `bulkmatch` | | `client.contacts.bulk_match()` | `contactmatch` | | `client.contacts.generate()` | `discogen` | | `client.discogen.process()` | `discogen` | | `client.discogen.process_personas()` | `discogen` | | `client.validate_icp()` | `discogen` | | `client.segment()` | `segment` | | `client.segment_file()` | `segment` | The task family is the path segment used when polling. Each of these reads `task_id` from the submit response. `wait()` polls until the task reaches a terminal status and returns the `JobStatus`: ```python from discolike import Discolike from discolike.requests import SegmentParams with Discolike() as client: job = client.segment(SegmentParams(domains="stripe.com,adyen.com")) status = job.wait(poll_interval=10, on_poll=lambda s: print(s.status, s.progress)) print(status.results) ``` Terminal statuses are `completed`, `failed`, and `cancelled`. `wait()` returns the status on `completed` and on `cancelled` (a cancelled task returns with empty results), and raises `JobFailedError` on `failed`, so check `status.status` before using the results. On timeout it raises `JobTimeoutError`; the task keeps running server-side, so calling `wait()` again on the same handle resumes. Email finding uses its own handle types, `EmailJob` and `EmailBatch`, with their own identifiers and `completed` / `failed` as their terminal statuses. Full signatures, polling defaults, cancellation, and rehydration are in [Types](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). Managed prospecting has a separate lifecycle: `client.prospecting.start()` drafts a plan, and `client.prospecting.wait(run_id)` returns at `proposed` for review and approval. Use `approve` with the reviewed version, `message` for steering or clarification, and `get` with event/message cursors for updates. See [prospecting](https://docs.discolike.com/sdk/reference/#prospecting) for sync/async signatures and saved results. ## Errors and retries Every error the SDK raises for an API problem derives from `DiscolikeError`, so a single `except DiscolikeError` catches all of them. Non-2xx responses are converted to a typed exception by status code; transport failures are converted to `APIConnectionError`. All exception classes are importable from the package root, and every one carries `.status_code` and `.payload`. Invalid request values never reach this layer: constructing a [request model](#request-models) with a bad value raises `pydantic.ValidationError`, which is not a `DiscolikeError`. The SDK's own `ValidationError` is the server's 400 or 422 answer. ```python from discolike import Discolike, DiscolikeError from discolike.requests import CompaniesDataParams with Discolike() as client: try: data = client.companies.data(CompaniesDataParams(domain="stripe.com")) except DiscolikeError as exc: print(f"request failed ({exc.status_code}): {exc}") data = None ``` Branch on a specific class when the recovery differs, and back off on `RateLimitError` using the server's own hint: ```python import time from discolike import Discolike, RateLimitError from discolike.requests import CompaniesScoreParams with Discolike() as client: for domain in ["stripe.com", "adyen.com", "checkout.com"]: while True: try: print(client.companies.score(CompaniesScoreParams(domain=domain)).score) break except RateLimitError as exc: time.sleep(exc.retry_after or 5.0) ``` The full hierarchy is in [Types](https://docs.discolike.com/sdk/reference/#exceptions). Retries are built into the transport. The loop runs `max_retries + 1` times, so the default is 4 total attempts. What is retried depends on the HTTP method: | Method | Retried statuses | Retried exceptions | |---|---|---| | `GET`, `DELETE` | 429, 502, 503, 504 | any transport failure: connect errors, read timeouts, write errors, protocol errors | | `POST`, `PUT`, `PATCH` | 429 only | connection errors only, where the request never reached the server | Backoff between attempts is the numeric `Retry-After` header when the response carries one, and otherwise exactly `0.5 * 2 ** attempt`: 0.5s, 1s, 2s for the default three retries, with no added jitter and no upper bound. Sleeps use `time.sleep` on the sync client and `asyncio.sleep` on the async one. After the last attempt the final response is converted to a typed exception: a persistent 429 becomes `RateLimitError`, a persistent 503 becomes `ServerError`, and an exhausted connect failure becomes `APIConnectionError`. A 429 for too much [work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress) carries `Retry-After: 60`, so with the default three retries a submit over the limit blocks for up to three minutes before raising `RateLimitError`. Its message gives your count and the limit. Pass `max_retries=0` to get the error at once and schedule the resubmit yourself. Job polling deadlines are a separate mechanism with their own `timeout` argument on `wait()`. ## Where to go next Every namespace and method is on the [Reference](https://docs.discolike.com/sdk/reference/) page. | Task | Entry point | Section | |------|-------------|---------| | Check requests, records, and spend | `client.account.*` | [account](https://docs.discolike.com/sdk/reference/#account) | | Look up firmographics, score, growth, or page text for a domain | `client.companies.*` | [companies](https://docs.discolike.com/sdk/reference/#companies) | | Search, look up, or match people | `client.contacts.*` | [contacts](https://docs.discolike.com/sdk/reference/#contacts) | | Run an LLM research pass over companies or personas | `client.discogen.*` | [discogen](https://docs.discolike.com/sdk/reference/#discogen) | | Find lookalike companies from seeds or a text ICP | `client.discover()`, `client.count()` | [discovery](https://docs.discolike.com/sdk/reference/#discovery) | | Find a work email from a name and a domain | `client.email.*` | [email](https://docs.discolike.com/sdk/reference/#email) | | Enrich a domain list, segment it, or validate an ICP | `client.append()`, `client.segment()`, `client.segment_file()`, `client.validate_icp()` | [enrich](https://docs.discolike.com/sdk/reference/#enrich) | | Bring your own LLM provider keys | `client.llm_providers.*` | [llm_providers](https://docs.discolike.com/sdk/reference/#llm_providers) | | Resolve a messy company name to a domain | `client.match.*` | [match](https://docs.discolike.com/sdk/reference/#match) | | Save, reuse, and exclude result sets | `client.queries.*` | [queries](https://docs.discolike.com/sdk/reference/#queries) | | Bring your own web search provider keys | `client.search_providers.*` | [search_providers](https://docs.discolike.com/sdk/reference/#search_providers) | | Read a request or response model's fields, a job handle, or an exception | `DiscolikeRequest`, `CompanyProfile`, `Job`, `DiscolikeError` | [Types](https://docs.discolike.com/sdk/reference/#types) | `discover`, `count`, `validate_icp`, `append`, `segment`, and `segment_file` sit directly on the client as convenience forwarders. Everything else lives under a namespace. Paging is explicit: page through results by incrementing `offset` on `client.discover()`, `client.contacts.search()`, `client.contacts.discover()`, and `client.queries.list()`. ## Reference Every method on `Discolike`, grouped by namespace and sorted alphabetically, followed by the models and exceptions in [Types](#types). New here? Start with [Getting Started](https://docs.discolike.com/sdk/) 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`](#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](https://docs.discolike.com/sdk/#request-models) for the migration from keyword arguments. Every method has an identical twin on `AsyncDiscolike`. ## account Account usage and quota. Wraps the [Usage endpoint](https://docs.discolike.com/api/endpoints/usage/). ### account.usage ```python client.account.usage() -> Usage ``` `GET /usage`. Takes no arguments. Returns [`Usage`](#usage). ```python from discolike import Discolike with Discolike() as client: usage = client.account.usage() print(usage.month_to_date_requests, usage.month_to_date_spend) ``` ``` 26 26.37 ``` ## 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 ```python client.companies.data(params: CompaniesDataParams) -> BizData ``` `CompaniesDataParams` is imported from `discolike.requests`. | Field | Type | Default | Description | |-------|------|---------|-------------| | `domain` | `str` | required | Domain to look up | Returns [`BizData`](#companyprofile). REST endpoint: [BizData](https://docs.discolike.com/api/endpoints/bizdata/). ```python from discolike.requests import CompaniesDataParams profile = client.companies.data(CompaniesDataParams(domain="stripe.com")) print(profile.name, profile.employees, profile.revenue_range) # Stripe 10001+ >1B ``` ### companies.score ```python client.companies.score(params: CompaniesScoreParams) -> Score ``` `CompaniesScoreParams` is imported from `discolike.requests`. | Field | Type | Default | Description | |-------|------|---------|-------------| | `domain` | `str` | required | Domain to look up | Returns [`Score`](#score). REST endpoint: [Score](https://docs.discolike.com/api/endpoints/score/). ```python from discolike.requests import CompaniesScoreParams score = client.companies.score(CompaniesScoreParams(domain="stripe.com")) print(score.score, score.first_event, score.parameters.base_score) # 701 2011-01-04 688.0 ``` ### companies.growth ```python client.companies.growth(params: CompaniesGrowthParams) -> Growth ``` `CompaniesGrowthParams` is imported from `discolike.requests`. | Field | Type | Default | Description | |-------|------|---------|-------------| | `domain` | `str` | required | Domain to look up | Returns [`Growth`](#growth). REST endpoint: [Growth](https://docs.discolike.com/api/endpoints/growth/). ```python from discolike.requests import CompaniesGrowthParams growth = client.companies.growth(CompaniesGrowthParams(domain="stripe.com")) print(growth.score_growth_3m, growth.subdomain_growth_3m) # 1.1 7.8 ``` ### companies.extract ```python client.companies.extract(params: CompaniesExtractParams) -> ExtractResult ``` `CompaniesExtractParams` 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`](#extractresult). REST endpoint: [Extract](https://docs.discolike.com/api/endpoints/extract/). ```python from discolike.requests import CompaniesExtractParams page = client.companies.extract(CompaniesExtractParams(domain="stripe.com")) print(page.language, page.text[:60]) # en Stripe | Financial Infrastructure to Grow Your Revenue Produ ``` ### companies.redirects ```python 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`](#redirect)`]`. REST endpoint: [Redirects](https://docs.discolike.com/api/endpoints/redirects/). ```python 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 ```python 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`](#vendor)`]`. REST endpoint: [Vendors](https://docs.discolike.com/api/endpoints/vendors/). ```python 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 ```python 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`](#subsidiary)`]`. REST endpoint: [Subsidiaries](https://docs.discolike.com/api/endpoints/subsidiaries/). ```python from discolike.requests import CompaniesSubsidiariesParams for row in client.companies.subsidiaries(CompaniesSubsidiariesParams(domain="stripe.com", match="parent"))[:2]: print(row.parent_domain, "->", row.child_domain, row.linked_score) # stripe.com -> stripe.network 0 # stripe.com -> stripecdn.com 686 ``` ### companies.public_links ```python 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`](#publiclink)`]`. REST endpoint: [PublicLink](https://docs.discolike.com/api/endpoints/publiclink/). ```python 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 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](https://docs.discolike.com/api/endpoints/contacts/); 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` | Every word of any one term must appear in the profile summary, in any order | | `negate_summary` | `str \| None` | `None` | Terms whose words, all present in the summary, exclude a contact | | `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, ISO 3166-2 code or full name. Resolved against every selected `person_country`; a value that resolves under none of them is dropped rather than rejected | | `has_email` | `bool \| None` | `False` | Only contacts with an email address | | `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](https://docs.discolike.com/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, ISO 3166-2 code or full name. Resolved against every selected `filter_country`; a value that resolves under none of them is dropped rather than rejected | | `negate_filter_state` | `list[str] \| None` | `None` | Company states to exclude, same format and resolution as `filter_state` | | `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](#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 | `email_validated` is deprecated. Treated as `has_email`: contact search no longer filters on email verification; verify emails instead. It is no longer a field on these models, but passing it still works: it goes out as an extra field, the API treats `true` as `has_email`, and the SDK raises a `DeprecationWarning`. Use `has_email` in new code. The `email_validated` field on each returned contact is `None` until the contact's email is verified. ### contacts.search ```python 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`](#contact)`]`, one per person. REST endpoint: [GET /contacts](https://docs.discolike.com/api/endpoints/contacts/). ```python from discolike.requests import ContactsSearchParams contacts = client.contacts.search( ContactsSearchParams( domain=["stripe.com"], seniority=["executive"], has_email=True, max_records=20 ) ) for person in contacts[:3]: print(person.persona_id, person.name, person.title, person.email) # 1321743327 William Nichols Strategy & Operations, Product william@stripe.com # 1321743234 Tom Silva Sales Leader - Stripe Platforms tom@stripe.com # 1321743193 Samuel Fuchs Platform Partnerships samuel@stripe.com ``` ### contacts.count ```python client.contacts.count(params: ContactsCountParams) -> Count ``` `ContactsCountParams` is imported from `discolike.requests`. The server honours the filter fields and ignores `max_records`, `max_companies`, and `offset`. Returns a [`Count`](#count) with the total at `.count`. REST endpoint: [GET /contacts/count](https://docs.discolike.com/api/endpoints/contacts/). ```python from discolike.requests import ContactsCountParams result = client.contacts.count(ContactsCountParams(seniority=["executive"], domain=["stripe.com"])) print(result.count) # 978 ``` ### contacts.lookup ```python client.contacts.lookup(params: ContactsLookupParams) -> Contact ``` `ContactsLookupParams` 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`](#contact). REST endpoint: [GET /contacts/lookup](https://docs.discolike.com/api/endpoints/contacts/). ```python from discolike.requests import ContactsLookupParams person = client.contacts.lookup(ContactsLookupParams(persona_id=319671825)) print(person.persona_id, person.name, person.title, person.domain) # 319671825 Patrick Collison CEO stripe.com ``` ### contacts.match ```python client.contacts.match(params: ContactsMatchParams) -> ContactMatchResponse ``` `ContactsMatchParams` 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`](#contactmatchresponse). Server-side constraints on `name` and `limit` are on [Contact Match](https://docs.discolike.com/api/endpoints/contact-match/). ```python from discolike.requests import ContactsMatchParams response = client.contacts.match(ContactsMatchParams(name="Patrick Collison", company_name="Stripe", limit=2)) for hit in response.matches: print(hit.match_score, hit.name, hit.title, hit.company_name) # 96.2 Patrick Collison CEO Stripe # 66.7 Patrick Collison Cofounder Arc Institute ``` ### contacts.bulk_match ```python client.contacts.bulk_match(request: BulkContactMatchRequest) -> Job ``` `BulkContactMatchRequest` 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`](#job-and-asyncjob) in the `contactmatch` family. REST endpoint: [Contact Bulk Match](https://docs.discolike.com/api/endpoints/contact-bulk-match/). ```python 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 ```python client.contacts.discover(request: ContactFilters) -> ContactsDiscoverResponse ``` `ContactFilters` is imported from `discolike.requests`. The grouped-by-company contact endpoint: the server honours every filter field, the three paging fields `search()` uses, and `results_by_company`, `include_search_contacts`, and `consensus`. Returns a [`ContactsDiscoverResponse`](#contactsdiscoverresponse--contactsbycompany): a `results` map of domain → [`ContactsByCompany`](#contactsdiscoverresponse--contactsbycompany) (firmographics plus nested contacts), with `total_contacts` and `total_domains` counters. REST endpoint: [POST /contacts/discover](https://docs.discolike.com/api/endpoints/contacts/). ```python 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 ```python client.contacts.generate(request: ContactGenerateRequest) -> Job ``` `ContactGenerateRequest` 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, or `native`. Omitted: DiscoLike Groove when it is available and a search provider resolves, otherwise your default LLM integration | | `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 | | `find_emails` | `bool \| None` | `False` | Run the email finder over named, email-less rows before the job completes; fills `email` and `email_status` on those rows. Found addresses bill under the finder's rules | Returns a [`Job`](#job-and-asyncjob) in the `discogen` family, not `contactmatch`. `POST /contacts/discover/generate`. ```python 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 Runs an LLM prompt against every record in a batch: one prompt, many domains or personas. Wraps the [DiscoGen endpoints](https://docs.discolike.com/api/endpoints/discogen/). `integration_id` and `search_provider_id` values come from [llm_providers](#llm_providers) and [search_providers](#search_providers). ### discogen.process ```python client.discogen.process(request: DiscoGenProcessRequest) -> Job ``` `DiscoGenProcessRequest` is imported from `discolike.requests`. Defaults in the tables below are the model's declared defaults and mirror the server's. A field you never set is not sent at all; the server default governs. | 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, or your only one, 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, or your only one, 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`](#job-and-asyncjob) in the `discogen` family. ```python 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(max_wait=1800, poll_interval=10) print(final.status, final.results) ``` ### discogen.process_personas ```python client.discogen.process_personas(request: DiscoGenPersonaProcessRequest) -> Job ``` `DiscoGenPersonaProcessRequest` 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`](#job-and-asyncjob) in the `discogen` family. ```python 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 ```python client.discogen.models() -> DiscogenModels ``` `GET /discogen/models`. Lists the LLM models available to you, grouped by provider. Takes no arguments and costs nothing. Returns [`DiscogenModels`](#discogenmodels--discogenmodelinfo). `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`. ```python 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 ```python 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-and-asyncjob). ```python job = client.discogen.job("d3f0c9a1-1f4b-4a67-9a10-6ba0d2f8e7c1") print(job.status().status) ``` ## 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](https://docs.discolike.com/api/endpoints/discover/) and [Count](https://docs.discolike.com/api/endpoints/count/) endpoints. ### discover ```python 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`, and the continents `EUROPE`, `ASIA`, `AFRICA`, `NORTH_AMERICA`, `SOUTH_AMERICA`, `OCEANIA`, `ANTARCTICA` | | `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. Requires exactly one `country` value; passing more than one rejects `state`. That value may be a region alias, in which case states resolve against every member country. See [States](https://docs.discolike.com/states/) | | `negate_state` | `list[str] \| None` | `None` | States to exclude, same format as `state`, up to 100 | | `geo` | `list[str] \| None` | `None` | Circular areas, each written `lat,lon` or `lat,lon,radius` (`"30.27,-97.74"`, `"30.27,-97.74,30mi"`). The radius takes a `km` or `mi` suffix, a bare number meaning kilometres; it defaults to 50km and may not exceed 1000km. A single circle may be passed as a bare string | | `bbox` | `list[str] \| None` | `None` | Bounding boxes, each written `min_lat,min_lon,max_lat,max_lon`. Longitudes may wrap the antimeridian by setting `min_lon` above `max_lon`. A single box may be passed as a bare string | | `lat` | `float \| None` | `None` | Latitude of a search centre, -90 to 90. Must be set together with `lon` | | `lon` | `float \| None` | `None` | Longitude of a search centre, -180 to 180. Must be set together with `lat` | | `radius` | `str \| None` | `None` | Radius around `lat`/`lon`: a number with an optional `km` or `mi` suffix (`"50"`, `"50km"`, `"30mi"`). Server default 50km, maximum 1000km | Every `geo` circle, every `bbox` and the circle formed by `lat`/`lon`/`radius` are OR'd together, up to 10 shapes per query, and the union is AND'd with the other filters. Shapes are validated locally before the request is sent, so a malformed box or circle raises `pydantic.ValidationError` rather than a server 4xx. Company filters: | Field | Type | Default | Description | |-------|------|---------|-------------| | `category` | `list[str] \| None` | `None` | Industry filter (`SOFTWARE`, `HEALTHCARE`, …), validated locally against the industry list. See [Industries](https://docs.discolike.com/industries/) | | `negate_category` | `list[str] \| None` | `None` | Industries to exclude, same values as `category` | | `sub_industry` | `list[str] \| None` | `None` | Second-level label scoped to a category. Bare label (`ROOFING`) or qualified key (`CONSTRUCTION/ROOFING`), case-insensitive, up to 50. A bare label with one unambiguous parent adds that parent to `category`. See [Sub-Industries](https://docs.discolike.com/sub-industries/) | | `negate_sub_industry` | `list[str] \| None` | `None` | Sub-industries to exclude, same format, up to 50. Does not affect `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 25 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](https://docs.discolike.com/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`](#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 | | `relevance` | `str \| None` | `None` | Where a lookalike list stops, at the edge of the ICP's market: `MAX_RELEVANCE` (shortest, tightest list), `BALANCED` (the app's default), `MAX_COVERAGE` (longest list, more borderline companies near the end). `None` means no automatic stop. Applies to searches driven by `icp_text` or `domain`. A stopped list can return an empty later page | | `include_search_domains` | `bool \| None` | `False` | Include the input `domain` values in the results | AI features: | Field | Type | Default | Description | |-------|------|---------|-------------| | `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 | `consensus` and `enhanced` are deprecated and ignored: discover applies consensus itself to single-domain lookalike searches without ICP text, and no longer adds AI-suggested domains. They are no longer fields on `DiscoverParams`, but passing either still works: it goes out as an extra field, the API ignores it, and the SDK raises a `DeprecationWarning`. `variance` is deprecated the same way: it is no longer a field on `DiscoverParams`, but passing it still sends it and raises a `DeprecationWarning`. The API maps it onto `relevance`: `LOW`, `MID_LOW`, `MEDIUM` act as `MAX_RELEVANCE`; `MID_HIGH`, `HIGH` as `BALANCED`; `UNRESTRICTED` as no stop. `relevance` wins when both are sent. Use `relevance` in new code. Returns `list[`[`Company`](#company)`]`. ```python from discolike.requests import DiscoverParams results = client.discover(DiscoverParams(domain=["stripe.com"], country=["US"], max_records=5)) for company in results: print(company.domain, company.similarity, company.name, company.employees) # speedysaas.com 90.0 Speedy SaaS 1-10 # bluesnap.com 90.0 BlueSnap 201-500 # zylopay.net 90.0 Zylopay 11-50 ``` ### count ```python client.count(params: CountParams) -> Count ``` `CountParams` is imported from `discolike.requests`. `CountParams` carries a strict subset of `DiscoverParams`'s fields, with the same types, defaults, constraints, and meanings: `phrase_match`, `negate_phrase_match`, `subdomain`, `negate_subdomain`, `tech_stack`, `negate_tech_stack`, `category`, `negate_category`, `sub_industry`, `negate_sub_industry`, `min_digital_footprint`, `max_digital_footprint`, `state`, `negate_state`, `country`, `negate_country`, `geo`, `bbox`, `lat`, `lon`, `radius`, `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`, `relevance`, `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. Defaults specific to `CountParams`: | Field | Type | Default | Description | |-------|------|---------|-------------| | `exclude_leadgen` | `bool \| None` | `False` | Counts cover every profile the filters match, including suspected lead-generation sites. Set it to `True` to leave them out | Returns [`Count`](#count-1). ```python from discolike.requests import CountParams total = client.count(CountParams(category=["SOFTWARE"], country=["US"], employee_range="51,200")) print(total.count) # 1651 ``` ## email Finds work email addresses from a name and a domain. Every call is asynchronous server-side: submitting returns a handle, and you poll that handle for the result. Email uses its own handle types, [`EmailJob` and `EmailBatch`](#emailjob-and-emailbatch), rather than the `Job` class the rest of the SDK returns. ### email.find ```python client.email.find(request: FindEmailRequest) -> EmailJob ``` `FindEmailRequest` 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`](#emailjob-and-emailbatch) carrying `job_id` and `kind` (always `"find"`). Its `wait()` returns an [`EnumerationOutput`](#enumerationoutput--enumerationmatch) rather than the [`EmailJobResult`](#emailjobresult) wrapper: the address is at `output.result.email`, and `output.status` is the enumeration outcome, not a job lifecycle status. ```python from discolike.requests import FindEmailRequest job = client.email.find(FindEmailRequest(first_name="Patrick", last_name="Collison", domain="stripe.com")) output = job.wait(max_wait=120.0, poll_interval=3.0) print(output.status, output.result.email, output.result.pattern) # catch_all_pattern patrick.collison@stripe.com first.last ``` ### email.find_batch ```python client.email.find_batch(request: FindEmailBatchRequest) -> EmailBatch ``` `FindEmailBatchRequest` 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`](#emailjob-and-emailbatch) with `kind="find"`. ```python 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(max_wait=300.0, poll_interval=5.0) print(results.total, results.completed, results.failed) ``` ### email.job ```python 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`](#emailjob-and-emailbatch). ```python job = client.email.job("ee58242e-2d7a-49cb-abae-5ed139aadbb6") current = job.status() print(current.status, current.result.result.email) # completed patrick.collison@stripe.com ``` ### email.batch ```python 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`](#enumerationoutput--enumerationmatch), `"verify"` as [`ValidationOutput`](#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`](#emailjob-and-emailbatch). Verify batches are created by the DiscoLike app (the verify endpoint is not available to API keys), then re-attached here by ID: ```python 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 `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 ```python client.append(params: AppendParams, *, file: pathlib.Path | str | BinaryIO | None = None) -> list[AppendResult] | bytes ``` `AppendParams` 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](https://docs.discolike.com/datasets/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](https://docs.discolike.com/api/endpoints/append/#billing) for the full rules. The return shape is decided by the response's `Content-Type` header: a header containing `application/json` yields `list[`[`AppendResult`](#appendresult)`]`, and anything else yields `bytes`, the raw response body. `csv=True` is what triggers the second case. ```python import io from discolike.requests import AppendParams rows = client.append(AppendParams(dataset=["bizdata"]), file=io.BytesIO(b"domain\nstripe.com\n")) for row in rows: print(row.domain, row.name, row.employees, row.revenue_range) # stripe.com Stripe 10001+ >1B ``` ### segment ```python client.segment(params: SegmentParams) -> Job ``` `SegmentParams` 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`](#job-and-asyncjob) 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`](#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](https://docs.discolike.com/datasets/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. ```python 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 ```python client.segment_file(params: SegmentFileParams, *, file: pathlib.Path | str | BinaryIO) -> Job ``` `SegmentFileParams` 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`](#job-and-asyncjob) whose `task_family` is `"segment"`; the job and its results behave exactly as documented under [`segment`](#segment). ```python 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 ```python client.validate_icp(request: ValidateIcpRequest) -> Job ``` `ValidateIcpRequest` 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, or your only integration, 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`](#job-and-asyncjob) whose `task_family` is `"discogen"`, not `"segment"`; validation runs on the DiscoGen pipeline and polls the DiscoGen status endpoint. It needs an [LLM provider](#llm_providers) configured on your account. Each domain comes back with three columns: | Column | Values | Description | |--------|--------|-------------| | `Fit` | `Yes`, `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 | ```python 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 Manages bring-your-own-key LLM integrations, wrapping the [LLM Providers endpoints](https://docs.discolike.com/api/endpoints/llm-providers/). The `integration_id` values these return are what you pass as `integration_id` to [discogen](#discogen) and [validate_icp](#validate_icp). For running the stack against your own infrastructure, see the [self-hosting guide](https://docs.discolike.com/guides/self-hosting/). ### llm_providers.list ```python client.llm_providers.list() -> LLMProviderList ``` `GET /llm-providers/config`. Takes no arguments. Returns [`LLMProviderList`](#llmproviderlist). ```python for provider in client.llm_providers.list().providers: print(provider.integration_id, provider.integration_name) # 1f5e8466-fda4-4d4e-88eb-4b0886c37003 grok-4.5-latest # 0ad8cd1a-70f8-48ee-ae53-42a0c8e94079 Perplexity fast search ``` ### llm_providers.get ```python 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`](#llmproviderconfig). ```python config = client.llm_providers.get(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003") print(config.to_dict()) ``` ### llm_providers.create ```python client.llm_providers.create(request: LLMProviderCreateRequest) -> LLMIntegrationResult ``` `LLMProviderCreateRequest` 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()`](#discogenmodels) | | `base_url` | `str \| None` | `None` | Override the provider endpoint, for gateways or self-hosted models | `POST /llm-providers/config`. Returns [`LLMIntegrationResult`](#llmintegrationresult). ```python 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 ```python client.llm_providers.update(request: LLMProviderUpdateRequest, *, integration_id: str) -> LLMIntegrationResult ``` `LLMProviderUpdateRequest` 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`](#llmintegrationresult). ```python 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 ```python 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`. ```python client.llm_providers.delete(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003") ``` ### llm_providers.set_default ```python 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`](#llmintegrationresult). ```python result = client.llm_providers.set_default(integration_id="1f5e8466-fda4-4d4e-88eb-4b0886c37003") print(result.message) ``` ### llm_providers.test_connection ```python client.llm_providers.test_connection(request: LLMProviderCreateRequest) -> LLMIntegrationResult ``` Takes the same `LLMProviderCreateRequest` as [`create`](#llm_providerscreate), 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`](#llmintegrationresult). ```python 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) ``` ## match 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 ```python client.match.company(params: MatchCompanyParams) -> MatchResponse ``` `MatchCompanyParams` 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, an ISO 3166-2 code or full name for any country that has subdivisions. Ignored if it cannot be resolved | | `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`](#matchresponse). Because `matches` defaults to an empty list rather than `None`, you can iterate it without a guard. ```python from discolike.requests import MatchCompanyParams response = client.match.company(MatchCompanyParams(name="Stripe", country="US", min_match_confidence=80)) for match in response.matches: print(match.domain, match.name, match.match_confidence) # stripe.com Stripe 100.0 ``` ### match.bulk ```python client.match.bulk(params: MatchBulkParams, *, file: pathlib.Path | str | BinaryIO) -> Job ``` `MatchBulkParams` 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 states, as ISO 3166-2 codes or full names for any country that has subdivisions. Values that cannot be resolved are ignored | | `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`](#job-and-asyncjob) 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. ```python 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(max_wait=1800, poll_interval=10) for row in status.results: print(row) ``` ## queries Manages saved queries: the named, reusable result sets you feed back into discovery, append, and segment as `query_id`. Wraps the [Queries endpoints](https://docs.discolike.com/api/endpoints/queries/). 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 ```python client.queries.list(params: QueriesListParams) -> SavedQueries ``` `QueriesListParams` 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`](#savedqueries). Its `count` is the total number of matching queries on the server, not the number returned in `results`. Page through by incrementing `offset`. ```python from discolike.requests import QueriesListParams saved = client.queries.list(QueriesListParams(max_records=3)) print(saved.count) for query in saved.results: print(query.query_name, query.action, query.domain_count) # 1916 # ContaGen: Owner, founder, executive, marketing and ecommerce contacts from selected companies thin_contagen 3 # USA law firms in United States discover 100 ``` ### queries.create_exclusion_list ```python client.queries.create_exclusion_list(request: CreateExclusionListRequest) -> QueryResult ``` `CreateExclusionListRequest` 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`](#queryresult). ```python 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 ```python client.queries.save_results(request: SaveResultsRequest) -> QueryResult ``` `SaveResultsRequest` 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`](#queryresult). ```python 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 ```python client.queries.update(request: UpdateQueryRequest, *, query_id: str) -> QueryResult ``` `UpdateQueryRequest` 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`](#queryresult). ```python from discolike.requests import UpdateQueryRequest client.queries.update( UpdateQueryRequest(query_name="Q3 target accounts"), query_id="43a6cf1c-859a-4a66-b144-65a38405cbfd" ) ``` ### queries.delete ```python 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`. ```python client.queries.delete(query_id="43a6cf1c-859a-4a66-b144-65a38405cbfd") ``` ## search_providers Manages bring-your-own-key web search integrations, wrapping the [Search Providers endpoints](https://docs.discolike.com/api/endpoints/search-providers/). The `integration_id` values these return are what you pass as `search_provider_id` to [discogen](#discogen) and [validate_icp](#validate_icp). ### search_providers.list ```python client.search_providers.list() -> SearchProviderList ``` `GET /search-providers`. Takes no arguments. Returns [`SearchProviderList`](#searchproviderlist). `api_key` comes back masked as `*****`, and `is_default` tells you which integration is used when you omit `search_provider_id`. ```python for provider in client.search_providers.list().providers: print(provider.integration_id, provider.integration_name) # fd467f7f-11c1-4475-923a-f2c3341e41bb Serper # 9d52cfb2-88e2-4a36-befb-a2d4d89ee0a1 Linkup Search ``` ### search_providers.create ```python client.search_providers.create(request: SearchProviderRequest) -> SearchProviderConfig ``` `SearchProviderRequest` is imported from `discolike.requests`. [`update()`](#search_providersupdate) 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()`](#search_providersmodels) | | `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`](#searchproviderconfig). ```python 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 ```python client.search_providers.update(request: SearchProviderRequest, *, integration_id: str) -> SearchProviderConfig ``` `SearchProviderRequest` is imported from `discolike.requests`; it is the same model [`create()`](#search_providerscreate) 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`](#searchproviderconfig). ```python 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 ```python client.search_providers.delete(*, integration_id: str) -> None ``` | Parameter | Type | Description | |-----------|------|-------------| | `integration_id` | `str` | Integration UUID. Required | `DELETE /search-providers/{integration_id}`. Returns `None`. ```python client.search_providers.delete(integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb") ``` ### search_providers.set_default ```python 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`](#searchproviderresult). ```python result = client.search_providers.set_default( integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb" ) print(result.message, result.integration_id) ``` ### search_providers.clear_default ```python 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`](#searchproviderresult). ```python client.search_providers.clear_default( integration_id="fd467f7f-11c1-4475-923a-f2c3341e41bb" ) ``` ### search_providers.models ```python client.search_providers.models() -> SearchModels ``` `GET /search-providers/models`. Lists the search models you can configure, grouped by provider, with per-query cost. Takes no arguments. Returns [`SearchModels`](#searchmodels--searchmodelinfo). ```python 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 Account creation for a person who has no DiscoLike account yet. Wraps the [Signup API](https://docs.discolike.com/api/endpoints/signup/). 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 ```python signup(*, email: str, first_name: str, last_name: str, agent: str | None = None, base_url: str = "https://api.discolike.com/v1", request_timeout: float = 60.0, http_client: Client | None = None, allow_new_email: bool = False) -> SignupResult ``` `POST /public/signup`. Creates a DiscoLike user and organization for `email`. No credential is returned: the person gets a confirmation email and logs in at [app.discolike.com](https://app.discolike.com). Returns [`SignupResult`](#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/` | | `base_url` | `str` | `https://api.discolike.com/v1` | API base URL | | `request_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. ```python 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 ```python await async_signup(*, email: str, first_name: str, last_name: str, agent: str | None = None, base_url: str = "https://api.discolike.com/v1", request_timeout: float = 60.0, http_client: AsyncClient | None = None, allow_new_email: bool = False) -> SignupResult ``` Identical to [`signup`](#signup), awaited. `http_client` takes an async client. ## Types Models, credentials, job handles, and exceptions. Response models subclass `DiscolikeModel`; request models subclass [`DiscolikeRequest`](#discolikerequest). ### 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.` | 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 `DiscolikeRequest` is the base for every model in `discolike.requests`, the request models the methods take. It is configured with `extra="allow"` and `populate_by_name=True`. Its `to_wire()` method produces the query string or JSON body the client sends, and follows four rules: - Only fields you set are sent. A field left at its default is omitted entirely, so the server's default governs. - A field you set to `None` explicitly is kept in the output. JSON-body routes send it as `null`; query-parameter routes drop it before the request, since a query string has no null. - Fields the model does not declare pass through unchanged. - A field declared with an alias is sent under its wire name, and accepts either the Python name or the alias on construction. ```python 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](#exceptions). `DiscolikeRequest` itself is importable from the package root, the models from `discolike.requests`: ```python from discolike import DiscolikeRequest from discolike.requests import MatchCompanyParams ``` The full set of models, by namespace: | Namespace | Model | Method | |-----------|-------|--------| | root | `DiscoverParams` | [`discover()`](#discover) | | root | `CountParams` | [`count()`](#count) | | root | `ValidateIcpRequest` | [`validate_icp()`](#validate_icp) | | root | `AppendParams` | [`append()`](#append) | | root | `SegmentParams` | [`segment()`](#segment) | | root | `SegmentFileParams` | [`segment_file()`](#segment_file) | | companies | `CompaniesDataParams` | [`companies.data()`](#companiesdata) | | companies | `CompaniesScoreParams` | [`companies.score()`](#companiesscore) | | companies | `CompaniesGrowthParams` | [`companies.growth()`](#companiesgrowth) | | companies | `CompaniesExtractParams` | [`companies.extract()`](#companiesextract) | | companies | `CompaniesRedirectsParams` | [`companies.redirects()`](#companiesredirects) | | companies | `CompaniesVendorsParams` | [`companies.vendors()`](#companiesvendors) | | companies | `CompaniesSubsidiariesParams` | [`companies.subsidiaries()`](#companiessubsidiaries) | | companies | `CompaniesPublicLinksParams` | [`companies.public_links()`](#companiespublic_links) | | match | `MatchCompanyParams` | [`match.company()`](#matchcompany) | | match | `MatchBulkParams` | [`match.bulk()`](#matchbulk) | | contacts | `ContactsSearchParams` | [`contacts.search()`](#contactssearch) | | contacts | `ContactsCountParams` | [`contacts.count()`](#contactscount) | | contacts | `ContactsLookupParams` | [`contacts.lookup()`](#contactslookup) | | contacts | `ContactsMatchParams` | [`contacts.match()`](#contactsmatch) | | contacts | `BulkContactMatchRequest` | [`contacts.bulk_match()`](#contactsbulk_match) | | contacts | `BulkContactMatchQueryItem` | One entry of `BulkContactMatchRequest.queries` | | contacts | `ContactFilters` | [`contacts.discover()`](#contactsdiscover) | | contacts | `ContactGenerateRequest` | [`contacts.generate()`](#contactsgenerate) | | discogen | `DiscoGenProcessRequest` | [`discogen.process()`](#discogenprocess) | | discogen | `DiscoGenPersonaProcessRequest` | [`discogen.process_personas()`](#discogenprocess_personas) | | email | `FindEmailRequest` | [`email.find()`](#emailfind) | | email | `FindEmailBatchRequest` | [`email.find_batch()`](#emailfind_batch) | | queries | `QueriesListParams` | [`queries.list()`](#querieslist) | | queries | `CreateExclusionListRequest` | [`queries.create_exclusion_list()`](#queriescreate_exclusion_list) | | queries | `SaveResultsRequest` | [`queries.save_results()`](#queriessave_results) | | queries | `UpdateQueryRequest` | [`queries.update()`](#queriesupdate) | | search_providers | `SearchProviderRequest` | [`search_providers.create()`](#search_providerscreate) and [`update()`](#search_providersupdate) | | llm_providers | `LLMProviderCreateRequest` | [`llm_providers.create()`](#llm_providerscreate) and [`test_connection()`](#llm_providerstest_connection) | | llm_providers | `LLMProviderUpdateRequest` | [`llm_providers.update()`](#llm_providersupdate) | ### CompanyProfile The base profile model. `BizData` (from [`companies.data`](#companiesdata)) is `CompanyProfile` with no added fields; [`Company`](#company) and [`MatchResult`](#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`. `state` is the ISO 3166-2 subdivision name (e.g. `California`), not the code. `vendors` arrives on company results as an extra field rather than a declared one. ### Company Returned by [`discover()`](#discover). [`CompanyProfile`](#companyprofile) plus one field: | Field | Type | Description | |-------|------|-------------| | `similarity` | `float \| None` | Similarity between the result and your query (0-100) | ### Count Returned by [`count()`](#count). | Field | Type | Description | |-------|------|-------------| | `count` | `int \| None` | Number of companies matching the filters | ### Score Returned by [`companies.score()`](#companiesscore). | 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 Returned by [`companies.growth()`](#companiesgrowth). | 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 Returned by [`companies.extract()`](#companiesextract). | Field | Type | Description | |-------|------|-------------| | `text` | `str \| None` | Extracted page text | | `language` | `str \| None` | Detected language code | ### Redirect Returned by [`companies.redirects()`](#companiesredirects). | 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 Returned by [`companies.vendors()`](#companiesvendors). | 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 Returned by [`companies.subsidiaries()`](#companiessubsidiaries). | 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 Returned by [`companies.public_links()`](#companiespublic_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 Returned by [`contacts.search()`](#contactssearch) and [`contacts.lookup()`](#contactslookup). | 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](#discolikemodel-and-extra-fields). ### ContactMatchResponse Returned by [`contacts.match()`](#contactsmatch). | 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 Returned by [`contacts.discover()`](#contactsdiscover). | 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`](#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 Returned by [`match.company()`](#matchcompany). | 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 [`CompanyProfile`](#companyprofile) plus one field: | Field | Type | Description | |-------|------|-------------| | `match_confidence` | `float \| None` | Match confidence, 0-100 | ### AppendResult Returned by [`append()`](#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](#discolikemodel-and-extra-fields), because the column set changes with the datasets you requested and the naming scheme the API picks. ```python row = rows[0] print(row.domain) print(row.model_dump(exclude_none=True)) ``` ```json { "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": "California", "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 What [`email.find()`](#emailfind) 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](https://docs.discolike.com/api/endpoints/email-find/). ### 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 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()`](#emailbatch) 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 Returned by [`discogen.models()`](#discogenmodels). | 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 Returned by [`queries.list()`](#querieslist). | Field | Type | Description | |-------|------|-------------| | `results` | `list[SavedQuery]` | The page of saved queries. Defaults to `[]` | | `count` | `int \| None` | Total matching queries on the server | #### 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 Returned by [`create_exclusion_list`](#queriescreate_exclusion_list), [`save_results`](#queriessave_results), and [`update`](#queriesupdate). | 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 | Field | Type | Description | |-------|------|-------------| | `providers` | `list[SearchProviderConfig]` | Configured search integrations. Defaults to `[]` | #### 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: ```python {'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 | Field | Type | Description | |-------|------|-------------| | `message` | `str \| None` | Server message | | `integration_id` | `str \| None` | Affected integration | ### 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 | Field | Type | Description | |-------|------|-------------| | `providers` | `list[LLMProviderConfig]` | Configured LLM integrations. Defaults to `[]` | | `mtime` | `str \| None` | Last modified timestamp of the configuration set | #### 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: ```python {'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 | Field | Type | Description | |-------|------|-------------| | `message` | `str \| None` | Server message | | `integration_id` | `str \| None` | Affected integration | | `status` | `str \| None` | Result status | ### Usage Returned by [`account.usage()`](#accountusage). 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](#discolikemodel-and-extra-fields): | 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 | ```python data = client.account.usage().to_dict() print(data["month_to_date_spend"], data["account_status"]) # 26.37 active ``` ### SignupResult Returned by [`signup`](#signup) and [`async_signup`](#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 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](https://docs.discolike.com/sdk/#authentication) for the resolution order and the file layout. ```python 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](https://docs.discolike.com/sdk/#oauth-sessions). ### Job and AsyncJob The handle returned by the seven job-returning methods listed in [Jobs and polling](https://docs.discolike.com/sdk/#jobs-and-polling). ```python class Job: task_family: str task_id: str def status(self) -> JobStatus: ... def cancel(self) -> None: ... def wait( self, *, max_wait: 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 | |---|---|---|---| | `max_wait` | `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 `max_wait=0`. - On `status == "failed"`, `wait()` raises `JobFailedError`. The message is `str(status.result)`, falling back to `"task failed"`; `exc.payload` is the full status dict. - On `status == "completed"` or `"cancelled"`, `wait()` returns the `JobStatus`. A cancelled task returns with empty results, so check `status.status` before using them. - On timeout, `wait()` raises `JobTimeoutError`. The task keeps running server-side. Call `wait()` again on the same handle to resume, or poll `status()` yourself later. `cancel()` issues the delete and returns immediately, without waiting for the task to stop. A `wait()` already running in another thread keeps polling until it observes the terminal status. To cancel from a poll callback, raise out of `on_poll`: ```python 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()`](#discogenjob) rehydrates a `discogen` handle from a task ID. For the `bulkmatch`, `contactmatch`, and `segment` families, construct a `Job` against the client's transport: ```python from discolike import Job job = Job(client._transport, task_family="segment", task_id=saved_task_id) ``` #### 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 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(*, max_wait=900.0, poll_interval=5.0, on_poll=None)` | `wait()` returns the unwrapped output for the handle's `kind`: [`EnumerationOutput`](#enumerationoutput--enumerationmatch) for find jobs and [`ValidationOutput`](#validationoutput) for verify jobs; `status()` returns [`EmailJobResult`](#emailjobresult) | | `EmailBatch` | `results(*, max_wait=900.0, poll_interval=5.0, on_poll=None)`, which polls and returns in one call | [`EmailBatchResults`](#emailjobresult) | Both take the same three keyword arguments: | Parameter | Type | Default | Description | | --------- | ---- | ------- | ----------- | | `max_wait` | `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 `max_wait=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 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: ```python 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 | | `InsufficientBalanceError` | `PlanAccessError` | HTTP 403 with `detail.code == "INSUFFICIENT_BALANCE"`: the request costs more than the remaining balance (see [Errors](https://docs.discolike.com/api/errors/#insufficient-balance)) | inherited, plus `max_affordable_records: int \| None`, `remaining_usd: float \| None`, `requested_records: int \| None`, `upgrade_url: str \| None` | | `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 `"; "` | | `{"detail": {"message": "...", ...}}` | that `message` | | 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()`](#append) called without `file` and without `query_id`, or [`segment()`](#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: ```python import pydantic from discolike.requests import MatchCompanyParams try: MatchCompanyParams(name="Acme", min_match_confidence=10) except pydantic.ValidationError as exc: print(exc.error_count(), exc.errors()[0]["loc"], exc.errors()[0]["msg"]) # 1 ('min_match_confidence',) Input should be greater than or equal to 50 ``` ```python from discolike import Discolike, AuthenticationError client = Discolike(api_key="dl_not_a_real_key") try: client.account.usage() except AuthenticationError as exc: print(exc, exc.status_code, exc.payload) # Invalid API Key or Session 401 {'detail': 'Invalid API Key or Session'} ``` The HTTP status codes behind these exceptions are documented in [API Errors](https://docs.discolike.com/api/errors/), and retry behavior is in [Getting Started](https://docs.discolike.com/sdk/#errors-and-retries). ## prospecting Managed prospecting starts by drafting a plan for approval. See the [Prospecting API](https://docs.discolike.com/api/endpoints/prospecting/) for lifecycle, billing, and accepted-result rules. Interpretation and wizards use platform credentials; coordination and contact qualification use customer LLM integrations. Work limits (`max_candidates`, `max_actions`) do not cap provider-dollar spending; set `max_provider_spend_usd` with [prospecting.update_plan](#prospectingupdate_plan) to cap what a run spends on your own AI and search provider keys. Every method below has an async twin with `await`. ### prospecting.start ```python from discolike.requests import ProspectingBrief client.prospecting.start(request: ProspectingBrief, *, idempotency_key: str) -> ProspectingRun ``` `POST /prospecting/runs`. The required keyword `idempotency_key` becomes the `Idempotency-Key` header (1–128 characters); retain it for retries. The run starts in `drafting` and waits at `proposed` for approval. Changing a request with the same key is rejected. The table lists model defaults. The SDK sends only fields you set: omitted counts can be inferred from the brief, while explicit values, including 1000 and 1, override the text. Candidate/action caps are automatic when omitted or `0`. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | brief | str | required | Company requirements and buyer roles, 10–4000 characters | | domains | list[str] | None | Explicit starting domains, up to 1000 | | company_names | list[str] | None | Company names to match, up to 100 | | customer_domains | list[str] | None | Your customers' domains, grouped into segments; lookalikes of each are found, up to 1000. Not with domains or company_names | | exclude_domains | list[str] | None | Suppressed domains, up to 1000 | | target_companies | int | 1000 | Companies with required contact coverage, 1–10000; omission allows inference | | contacts_per_company | int | 3 | Accepted contacts per company, 1–10; omission allows inference | | max_candidates | int | 0 | Automatic at 0, otherwise a cap up to 100000 | | max_actions | int | 0 | Automatic at 0, otherwise a cap up to 10000 | | validation_integration_id | str | None | Validation integration or configured default; supports explicit native-icp | | contact_integration_id | str | None | Contact research integration or configured default; supports explicit native | | search_provider_id | str | None | Search integration or configured default | | segment | bool | false | Optional segmentation | | checkpoints | str | "auto" | "ask" pauses at checkpoints; "auto" never pauses. See the [Prospecting API's Checkpoints section](https://docs.discolike.com/api/endpoints/prospecting/#checkpoints) | | deliverable | str | "leads" | "leads" finds people at the companies; "accounts" returns the checked companies only | | goal | str | "leads" | "leads" keeps adding rounds until the target is met; "companies" takes target_companies matching companies and works only those | ### prospecting.list ```python from discolike.requests import ProspectingListParams client.prospecting.list(params: ProspectingListParams | None = None) -> list[ProspectingRunSummary] ``` `GET /prospecting/runs`. Lists recent organization runs, newest first. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | limit | int | 20 | Number of runs, 1–50 | | before | UUID | None | Page past this run ID (the last run ID from a prior page); runs are ordered by creation time, then run ID, descending | ### prospecting.approve ```python from discolike.requests import ProspectingApproveRequest client.prospecting.approve(run_id: str | UUID, request: ProspectingApproveRequest) -> ProspectingRun ``` `POST /prospecting/runs/{run_id}/approve`. Review the plan before approving its exact version. Repeating approval of the already approved version is safe; a stale version returns a conflict. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | plan_version | int | required | Version you reviewed, at least 1 | | checkpoints | str | None | Optional "ask" or "auto" override; omit or send None to keep the brief's value | | seed_segments | list[int] | None | Ids from the plan card's seed_segments to expand; omit or send None to keep the run's current selection (the card's default, or whatever a chat edit set). An empty list is rejected | | segment | bool | None | Optional override of the brief's segment setting for this run; omit or send None to keep it | ### prospecting.message ```python from discolike.requests import ProspectingMessageRequest client.prospecting.message(run_id: str | UUID, request: ProspectingMessageRequest, *, idempotency_key: str) -> ProspectingMessage ``` `POST /prospecting/runs/{run_id}/messages`. Send steering, a clarification answer, or a question. The required keyword `idempotency_key` is a per-message key; reuse it when retrying that message. The returned object is the stored user message. Poll `get` with `messages_after` for the agent's reply. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | text | str \| None | none | Message text, 1–4000 characters. Send `text` or `intake` | | intake | dict[str, IntakeAnswer] \| None | none | Answers to an intake card, keyed by question key. See [prospecting.answer_intake](#prospectinganswer_intake) | | question_seq | int \| None | none | The `seq` of the question message a suggested reply answers (1 or more). If a newer question was posted since, the reply answers nothing. Required for "Find emails" at a `contacts_review` to count | ### prospecting.answer_intake ```python from discolike.requests import IntakeAnswer client.prospecting.answer_intake(run_id: str | UUID, answers: dict[IntakeKey, IntakeAnswer], *, idempotency_key: str, summary: str | None = None) -> ProspectingMessage ``` `POST /prospecting/runs/{run_id}/messages` with `intake`. Answers the intake card a run posts when a brief leaves out details (a `question` message whose `data["reason"]` is `"intake"`; its `data["fields"]` lists the questions and their options). `answers` maps each question key (`company_activity`, `industry`, `geography`, `company_size`, `persona_roles`, `list_size`) to an `IntakeAnswer` with `values` (option values from the card) and/or `other` (free text, up to 200 characters). Reuse `idempotency_key` when retrying. `summary` is optional message text; the stored message carries a summary of your choices either way. An answer that does not fit the card raises `ValidationError` (HTTP 422, the message names the field); answering a card that is no longer open raises `DiscolikeError` with `status_code` 409. See [Intake](https://docs.discolike.com/api/endpoints/prospecting/#intake). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | answers | dict[str, IntakeAnswer] | required | Answers keyed by question key; each has `values` and `other` | | summary | str \| None | none | Optional message text | ### prospecting.get ```python from discolike.requests import ProspectingGetParams client.prospecting.get(run_id: str | UUID, params: ProspectingGetParams | None = None) -> ProspectingRun ``` `GET /prospecting/runs/{run_id}`. The same offset and limit apply independently to companies and contacts. Use the returned `next_event_seq` and `next_message_seq` on subsequent polls. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | offset | int | 0 | Nonnegative result offset | | limit | int | 100 | Page size, 1–500 | | events_after | int | 0 | Return events after this nonnegative sequence | | messages_after | int | 0 | Return messages after this nonnegative sequence | ### prospecting.update_plan ```python from discolike.requests import ProspectingPlanSettings client.prospecting.update_plan(run_id: str | UUID, request: ProspectingPlanSettings) -> ProspectingRun ``` `PATCH /prospecting/runs/{run_id}/plan`. Changes a proposed plan's engines, size, deliverable and spending limits, then returns the run with the re-estimated plan at a new `plan_version`; approve that version. Option ids come from the plan message's `contact_engine`, `company_check_engine` and `search_provider`. An id outside the options raises `ValidationError` (422); a run that is not awaiting approval, a stale `plan_version`, or a run that cannot switch to accounts raises a conflict (409). | Parameter | Type | Default | Description | | --- | --- | --- | --- | | plan_version | int | required | Version you reviewed, at least 1 | | contact_integration_id | str | None | Contact research engine, an option id from the plan; omit to keep the current choice | | validation_integration_id | str | None | Company check engine, an option id from the plan; omit to keep the current choice | | search_provider_id | str | None | Search provider, an option id from the plan; "none" skips web research and finds indexed contacts only | | max_spend_usd | float | None | Most the run may spend on DiscoLike records and per-request fees, in USD at your plan's rates, at least 0; 0 removes the limit, omit to keep it; a plan with no per-record price raises `ValidationError` (422) | | max_provider_spend_usd | float | None | Most the run may spend on your own AI and search provider keys, in USD, at least 0; batches already running finish, so the total can end slightly above it; 0 removes the limit, omit to keep it | | target_companies | int | None | How many qualified companies the run aims for, 1–10000; the plan's limits are re-derived around it; omit to keep it | | contacts_per_company | int | None | Most contacts to find at each qualified company, 1–10; omit to keep it | | deliverable | str | None | "leads" finds people at the companies; "accounts" returns the checked companies only; omit to keep it | | goal | str | None | "leads" keeps adding rounds until the target is met; "companies" takes target_companies matching companies and works only those; omit to keep it | ### prospecting.rename ```python from discolike.requests import ProspectingRunUpdate client.prospecting.rename(run_id: str | UUID, request: ProspectingRunUpdate) -> ProspectingRunSummary ``` `PATCH /prospecting/runs/{run_id}`. Renames the run, switches its checkpoint mode, or both, and returns its summary, the same shape `list` returns. Set `title`, `checkpoints`, or both. A title alone works in any status; a title set before the agent plans is kept, and result lists the run already saved keep their names. `checkpoints` switches an unfinished run between Manual and Auto; switching to `"auto"` while the run waits on a checkpoint answers it with continue and resumes. A mode switch needs the same plan, quota and payment method as `start` (`PlanAccessError` otherwise), and a finished run, or resuming while another of your runs is active, raises `ValidationError` (409). A missing or deleted run raises `NotFoundError`. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | title | str | None | New title, 1–80 characters after surrounding whitespace is trimmed | | checkpoints | str | None | "ask" (Manual) pauses at later checkpoints; "auto" (Auto) never pauses | ### prospecting.cancel ```python client.prospecting.cancel(run_id: str | UUID) -> ProspectingRun ``` `POST /prospecting/runs/{run_id}/cancel`. Stops scheduling work; partial results and incurred charges remain. Active children can continue draining after cancellation. The run stays readable through `get` and `list`. ### prospecting.delete ```python client.prospecting.delete(run_id: str | UUID) -> None ``` `DELETE /prospecting/runs/{run_id}`. Cancels the run if it is still active, then removes it: `list` omits it and `get` raises `NotFoundError`. Charges already incurred are not refunded; saved result lists (`saved_query_ids`) remain available. Starting a new run with the deleted run's `idempotency_key` is rejected with a conflict. ### prospecting.wait ```python client.prospecting.wait(run_id: str | UUID, *, max_wait: float = 3600, poll_interval: float = 5) -> ProspectingRun ``` Returns the first page when status is `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. A proposed plan requires `approve` before research starts. Inspect `stop_reason`; completion does not guarantee the target was reached. A timeout raises `JobTimeoutError` and stops local polling only. The minimum poll interval is five seconds. Fetch additional pages with `get`. ### ProspectingRun Import response types from `discolike.resources.prospecting`. Statuses are `drafting`, `proposed`, `queued`, `running`, `needs_input`, `completed`, `failed`, and `cancelled`. | Field group | Fields | | --- | --- | | Lifecycle | `run_id`, `status`, `stop_reason`, `stage`, `phase`, `created_at`, `updated_at`, `error`, `waiting_for_worker` | | Plan | `brief`, `title`, `target_companies`, `contacts_per_company`, `plan`, `plan_version`, `approved_plan_version`, `last_decision` | | Progress | `events`, `next_event_seq`, `messages`, `next_message_seq`, `reply_pending`, `in_flight` | | Results | `companies`, `contacts`, `total_companies`, `total_contacts`, `qualified_companies`, `accepted_contacts`, `fit_companies`, `emails_found`, `offset`, `limit`, `saved_query_id` | | Work limits | `actions_used`, `max_actions` | `plan` is a `ProspectingPlan` with `company_queries`, `contact_filters`, `company_criteria`, `persona_criteria`, and `issues`. `ProspectingEvent` has `seq`, `created_at`, `stage`, `kind`, `message`, and `data`. `ProspectingMessage` has `seq`, `created_at`, `role`, `kind`, `content`, and `data`; the role is `user` or `agent`. `ProspectingInFlight` has `stage`, `items`, `plan_version`, `state`, and `started_at`. `ProspectingRunSummary` contains `run_id`, `status`, `title`, `stop_reason`, `stage`, a shortened `brief`, both target counts, `qualified_companies`, `accepted_contacts`, `created_at`, and `updated_at`. Date fields are Python `datetime` values; run and saved query IDs are `UUID` values. Saved snapshots may lag current progress; keep raw results separate from accepted counts. --- # CLI ## Getting Started `discolike` is the official command-line tool for the DiscoLike API. It exposes company discovery, enrichment, contact search, email finding, and account management as terminal commands that print JSON, so you can wire the API into shell scripts and cron jobs without writing Python. The CLI ships as its own PyPI distribution, `discolike-cli`, and installs a single executable named `discolike`. It depends on the [`discolike` SDK](https://docs.discolike.com/sdk/), which does the HTTP work and owns credential resolution. ## Requirements | Item | Value | |-------|-------| | Python | 3.11 or newer | | Distribution | `discolike-cli` | | Command installed | `discolike` | | Dependencies | `discolike==0.3.0` (pinned exactly, released together), `typer>=0.12`, `rich>=13.0` | | Current version | 0.3.0 | ## Install ### pip ```bash pip install discolike-cli ``` If you also want the SDK in the same environment, install it through the SDK's `cli` extra instead; it pulls in `discolike-cli` alongside the library: ```bash pip install "discolike[cli]" ``` ### uv tool Install the CLI as a standalone tool, isolated from your project environments: ```bash uv tool install discolike-cli ``` `uv tool install` puts `discolike` on your `PATH` in its own virtual environment. To upgrade later: ```bash uv tool upgrade discolike-cli ``` ### uvx (no install) Run the CLI without installing anything permanently: ```bash uvx --from discolike-cli discolike --version ``` The `--from` flag is required because the distribution name (`discolike-cli`) differs from the command name (`discolike`). Confirm the install and see both version numbers at once: ```bash discolike --version ``` ``` 🪩 discolike-cli 0.3.0 (discolike 0.3.0) ``` The first number is the CLI, the second is the SDK underneath it. They are released together and the CLI pins the SDK to the exact same version. ## Global options These options belong to the root command and must appear **before** the subcommand: ```bash discolike --api-key dk_... count --country US ``` | Flag | Type | Default | Environment variable | Description | |------|------|---------|----------------------|-------------| | `--api-key` | TEXT | none | `DISCOLIKE_API_KEY` | API key. Overrides the config file saved by `discolike auth login`. | | `--base-url` | TEXT | `https://api.discolike.com/v1` | none | API base URL. | | `--version` | flag | none | none | Print CLI and SDK versions and exit. | | `--install-completion` | flag | none | none | Install completion for the current shell. | | `--show-completion` | flag | none | none | Show completion for the current shell, to copy it or customize the installation. | | `--help` | flag | none | none | Show help and exit. | Every option in the CLI is long-form. There are no single-letter aliases anywhere, and `DISCOLIKE_API_KEY` is the only environment variable the CLI reads. `--base-url` points the CLI at a different API host: a local test server, or a proxy. Leave it unset to use production. ## Authentication Every command needs a credential. On a workstation, log in once through the browser: `discolike auth login` opens the DiscoLike authorization page, you approve access, and the CLI saves the resulting OAuth session to a config file it refreshes on its own from then on. Where there is no browser, such as servers and CI, use an API key instead; the CLI resolves one from the command line, the environment, or the same config file. 1. Log in through the browser. ```bash discolike auth login ``` ``` Open this URL in your browser to log in: https://auth.discolike.com/oauth/2.1/authorize?response_type=code&client_id=... ``` The CLI starts a temporary listener on `127.0.0.1`, opens the URL, and waits up to 180 seconds for the browser to be redirected back. Approve the request and the terminal prints the success payload: ```json {"logged_in": true, "method": "oauth", "expires_at": "2026-08-28T20:15:00+00:00"} ``` The session is verified against the account usage endpoint before anything is written. `--no-browser` prints the URL without opening a browser, and `--port` pins the loopback port so it can be forwarded over SSH; both are described under [auth login](https://docs.discolike.com/cli/reference/#auth-login). To use an API key instead, create one at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys) and pass it with `--api-key`, or pass `--method api_key` to be prompted for it with hidden input: ```bash discolike auth login --api-key dk_... ``` ```bash discolike auth login --method api_key ``` The key is verified the same way. A bad key exits 3 and writes nothing to disk. 2. Confirm which credential is active. ```bash discolike auth status ``` ```json { "source": "config", "method": "oauth", "expires_at": "2026-08-28T20:15:00+00:00", "expired": false, "valid": true } ``` `source` is `option` for a key passed as `--api-key`, `env` when `DISCOLIKE_API_KEY` is set, and `config` when the credential came from the config file. `method` is `oauth` or `api_key`. For an API key the payload carries `api_key`, masked to its last four characters (`"…isco"`; the leading `…` is JSON-escaped because the CLI emits ASCII-safe JSON), instead of `expires_at` and `expired`. `expired` only says whether the access token has passed its expiry; the session is refreshed on the next command regardless, so `"expired": true` next to `"valid": true` is normal. 3. Remove the saved credential when you are done. ```bash discolike auth logout ``` ```json {"logged_out": true} ``` Logging out deletes the session or key but keeps the CLI's OAuth client registration, so the next login skips the browser consent screen. It is safe to run when nothing is saved. ### Browser consent and re-registration On the first browser login the CLI registers itself with the authorization server as a public OAuth client (PKCE, no client secret) and saves the registration as `oauth_client` in the config file. The authorization server remembers your consent per client, so reusing that registration is what lets later logins go straight from the browser back to the terminal without asking again. A registration is reused when it was issued by the same authorization server as the `--base-url` in use and its loopback port can be bound again. The CLI registers a fresh client, and the browser asks for consent once more, when: - you log in against a different server with `--base-url`, since registrations are per server; - the port recorded in the saved registration is busy, or you pass a `--port` that differs from it. The server matches the redirect URI literally, port included, so a registration cannot be reused on another port; - the authorization server no longer recognises the saved client (`invalid_client` or `unauthorized_client`). The CLI discards the registration, registers a fresh one, and runs the browser flow once more. If that fresh client is rejected too, the login fails with a `LoginError`. ### Credential precedence The SDK resolves the credential on every command and uses the first source that yields one: | Order | Source | How to set it | |-------|--------|---------------| | 1 | The `--api-key` global flag | `discolike --api-key dk_... count --country US` | | 2 | The `DISCOLIKE_API_KEY` environment variable | `export DISCOLIKE_API_KEY=dk_...` | | 3 | The config file written by `auth login`, holding an OAuth session or an API key | `discolike auth login` | An API key from the flag or the environment always wins, even when an OAuth session is saved. If none of the three yields a credential, the command fails before any request is sent: ``` No API key found. Set the DISCOLIKE_API_KEY environment variable, pass api_key=..., or run `discolike auth login`. Create a key at https://app.discolike.com/account/management/keys ``` That is an `AuthenticationError`, so the exit code is 3. Both `auth login` and `auth status` honour `--base-url`: `auth login` logs in to that host's authorization server, and `auth status` verifies against it. An explicit `--api-key` on the command line switches `auth login` to the API-key flow with that key; an ambient `DISCOLIKE_API_KEY` does not, so the browser flow still runs in an environment that happens to have a key set. ### The config file | Item | Value | |------|-------| | Path | `${XDG_CONFIG_HOME:-~/.config}/discolike/config.json` | | Contents | One of the two shapes below, selected by `auth_method` | | Permissions | `0600`, owner read/write only | ```json {"auth_method": "api_key", "api_key": "dk_..."} ``` ```json { "auth_method": "oauth", "oauth": { "access_token": "eyJ...", "refresh_token": "...", "expires_at": 1756426800.0, "client_id": "...", "token_endpoint": "https://auth.discolike.com/oauth/2.1/token" }, "oauth_client": { "client_id": "...", "redirect_uri": "http://127.0.0.1:51234/callback", "issuer": "https://auth.discolike.com/oauth/2.1" } } ``` `oauth` is the session the SDK sends and refreshes; a refresh rewrites it with the rotated tokens. `oauth_client` is the CLI's client registration, kept across `auth logout` and carried over when you log in with an API key, so it is present in either shape once you have logged in through the browser. The file is created with mode `0600` and re-chmodded after each write, so it is never group- or world-readable. A missing, unreadable, or malformed config file, or an `oauth` section with a missing field, is treated as "no credential found" rather than an error, which means a corrupt file degrades to the environment variable or to the guidance message above. For CI and containers, prefer the environment variable over the config file: nothing has to be written to the image, and `auth status` will report `source: env`. Key creation, rotation, and revocation are covered in [API access](https://docs.discolike.com/api/access/). ## First call With a key in place, the cheapest useful command is a count, which returns a single number and bills nothing per record: ```bash discolike count --country US --category software ``` ```json { "count": 68900 } ``` From here: - [Command Reference](https://docs.discolike.com/cli/reference/): every command and option, grouped the way the CLI groups them. - [Output and scripting](#output-and-scripting): `--format`, exit codes, stderr, and piping results into `jq`. - [Common workflows](#common-workflows): end-to-end recipes that chain several commands. - [Python SDK](https://docs.discolike.com/sdk/): the same API as a library, with typed models. ## Output and scripting Every DiscoLike command prints machine-readable JSON by default when its output is not a terminal, writes errors to stderr as a single JSON object, and signals the error class through the exit code. That combination is what makes the CLI usable from a shell script without parsing prose. ### Output formats There are two formats: pretty-printed JSON and a Rich-rendered table. `--format` selects between them: | Value | Result | |-------|--------| | `table` | Render a table if the payload qualifies; otherwise JSON | | `json` | JSON | | anything else | JSON | | omitted | Table if stdout is a TTY and the payload qualifies; otherwise JSON | `--format` is free text, and only the exact string `table` changes the output; every other value, `--format json` included, produces JSON. For CSV, use `append --csv --output FILE`, which writes the API's CSV bytes to the file verbatim. #### TTY auto-selection When you omit `--format`, the CLI checks whether stdout is a terminal. Interactively you get a table; the moment you pipe or redirect, you get JSON. Nothing else about the command changes, so the same invocation is safe to develop interactively and then drop into a script. #### When a table is possible A table is only rendered when the payload is a **non-empty list of objects with at least one column that is scalar in every row**. Most DiscoLike endpoints return an object with the list nested under a key (`count` returns `{"count": ...}`, `queries list` returns `{"results": [...]}`, `search-providers list` returns `{"providers": [...]}`), and an object is never tabular. Those commands print JSON no matter what you pass: ```bash discolike count --country US --category software --format table ``` ```json { "count": 68900 } ``` `discover` is the main command that returns a bare list, so it is the one where `--format table` and the interactive default actually produce a table. Even there the table is partial: nested fields such as `address`, `keywords`, and `business_model` are not scalar in every row, so they are dropped, and only the first 8 surviving columns are shown. When a table is rendered: | Rule | Behavior | |------|----------| | Column selection | A key becomes a column only if its value is a string, number, boolean, or null in *every* row. One nested value anywhere drops the whole column. | | Column limit | The first 8 qualifying columns | | Cell truncation | Values longer than 80 characters are cut and suffixed with `…` | | Null cells | Rendered as an empty cell | Because columns are dropped rather than flattened, the table is a preview, not a complete view of the record. Use JSON whenever you need the full payload. ### stdout and stderr | Stream | Carries | |--------|---------| | stdout | Command results, either JSON or the rendered table | | stderr | Error payloads, `--wait` progress lines, and the `auth login` success payload | Errors are written as a **single line** of JSON to stderr: ```json {"error": "AuthenticationError", "message": "Invalid API Key or Session", "status_code": 401} ``` | Field | Type | Notes | |-------|------|-------| | `error` | string | The exception class name | | `message` | string | The server's message, extracted from the response body | | `status_code` | integer or null | The HTTP status, or null when the failure never reached the server | | `retry_after` | number | Present **only** on a `RateLimitError`, and only when the response carried a numeric `Retry-After` header | | `max_affordable_records`, `remaining_usd` | integer, number | Present **only** on an `InsufficientBalanceError` (`"code": "insufficient_balance"`, exit 3): the largest size the same request could ask for within the remaining balance (in the request's own unit, which the message names), and that balance in USD | Nothing is written to stdout when a command fails, so `cmd > out.json` leaves an empty file rather than a half-written one: ```bash discolike --api-key dk-bogus count --country US 2>/dev/null; echo "exit=$?" ``` ``` exit=3 ``` Progress from `--wait` also goes to stderr, one line per poll: ``` progress: 40% ``` That keeps stdout a clean JSON document even for long-running jobs. ### Exit codes | Exit code | Condition | HTTP status | |-----------|-----------|-------------| | 0 | Success | none | | 1 | Server error, or any unmapped failure | 5xx, or any other status | | 1 | Browser login failure (`LoginError`): timeout, denied consent, or an invalid callback | none | | 2 | Validation error (client-side or server-side), **or** a command-line usage error | 400, 422, or none | | 3 | Authentication error | 401 | | 3 | Plan access error | 402, 403 | | 4 | Rate limited | 429 | | 5 | Network or connection failure | none | | 6 | Not found | 404 | Exit code 2 covers three cases: a request the server rejected as invalid, a request the CLI rejected before sending anything, and a command line the CLI could not parse. Every option and `--param` value is validated against the API's own schema before the request is built, so an unknown enum value, a number outside its range, or a missing required option fails locally with the same JSON shape as a server error and `"status_code": null`: ```bash discolike discover --icp-prompt "B2B payments" --max-records 1; echo "exit=$?" ``` ``` {"error": "ValidationError", "message": "max_records: Input should be greater than or equal to 5", "status_code": null} exit=2 ``` Bad flags, an unknown command, and a malformed `--param` are usage errors: ```bash discolike count --param bogus; echo "exit=$?" ``` ``` Invalid value: --param must be in KEY=VALUE form, got 'bogus' exit=2 ``` Usage errors are rendered by the CLI framework as a bordered panel on stderr, not as a single-line JSON object. Distinguish the two by parsing: if stderr is valid JSON, it was a validation or API error (`status_code` is `null` when the request never left the machine); if not, it was a usage error. ### Scripting #### Piping to jq Because non-TTY output is always JSON, no flag is needed: ```bash discolike count --country US --category software | jq -r '.count' ``` ``` 68900 ``` Field extraction from a list-returning command works the same way: ```bash discolike discover --icp-prompt "B2B payments infrastructure" --max-records 25 \ | jq -r '.[].domain' ``` #### Checking exit codes Branch on the class of failure rather than on the message text: ```bash #!/usr/bin/env bash set -uo pipefail if out=$(discolike count --country US --category software 2>err.json); then echo "count: $(jq -r '.count' <<<"$out")" else case $? in 3) echo "auth or plan problem: $(jq -r .message err.json)" >&2; exit 1 ;; 4) sleep "$(jq -r '.retry_after // 60' err.json)"; exec "$0" ;; 5) echo "network failure, retrying later" >&2; exit 75 ;; *) echo "failed: $(jq -r .message err.json)" >&2; exit 1 ;; esac fi ``` Note the `2>err.json`: the error JSON is on stderr, so it has to be captured separately from the result. #### Async jobs: `--wait` versus polling Seven commands start an async job and take `--wait` and `--timeout`: `match` (with `--file`), `validate-icp`, `segment`, `contacts bulk-match`, `contacts generate`, `discogen run`, and `discogen run-personas`. Without `--wait`, the command returns immediately with the task handle and a ready-made polling command: ```json { "task_id": "0f2c...", "task_family": "bulkmatch", "hint": "poll with: discolike discogen status 0f2c... --family bulkmatch" } ``` Capture the id and poll on your own schedule: ```bash task_id=$(discolike match --file names.csv | jq -r '.task_id') until discolike discogen status "$task_id" --family bulkmatch \ | jq -e '.status | IN("completed", "failed", "cancelled")' >/dev/null; do sleep 30 done ``` With `--wait`, the command blocks, streams `progress: N%` lines to stderr, and prints the finished results to stdout. `--timeout` caps the wait in seconds and defaults to `900.0`: ```bash discolike match --file names.csv --wait --timeout 1800 > matched.json ``` Choose `--wait` for a job that fits inside one process: a cron entry, a CI step. Choose the polling form when the job may outlive the shell, or when you want to start several jobs and collect them later. `discogen status` and `discogen cancel` work for **any** task family, not just DiscoGen. Pass the `--family` value the job handed you. `--family` accepts `discogen`, `bulkmatch`, `contactmatch`, or `segment`, and defaults to `discogen`. The terminal statuses are `completed`, `failed`, and `cancelled`. ### The `--param` escape hatch `discover`, `count`, `contacts search`, `contacts count`, and `contacts discover` accept `--param KEY=VALUE`, which forwards an arbitrary parameter to the API. Use it for a query parameter the CLI has no flag for, so a new API capability does not require a CLI upgrade. ```bash discolike discover --icp-prompt "B2B payments" --param min_similarity=200 ``` Rules: | Rule | Behavior | |------|----------| | Splitting | Split on the **first** `=`. The value may contain further `=` characters. | | Lists | A value containing a comma becomes a list. `--param social=linkedin,youtube` sends `social=linkedin&social=youtube`. | | Repeatable | Pass `--param` more than once for multiple parameters. | | Precedence | A named flag **overrides** `--param` for the same key. | | Malformed | A value with no `=` raises a usage error and exits 2. | | Unknown key | A key the CLI does not know is forwarded to the API unchanged, so a new API parameter works without a CLI upgrade. | | Known key, bad value | A value outside the API schema (unknown enum value, number out of range) is rejected before the request and exits 2. | Comma splitting is unconditional: a value containing a comma is always sent as a list. Use a named flag when you need to send a single value that contains a comma. Precedence in practice: the named flag wins and the `--param` value is discarded: ```bash discolike count --country US --param country=DE --category software ``` ```json { "count": 68900 } ``` That is the US count, not the German one; `--country US` won. A bad value for a known key fails fast rather than reaching the server: ```bash discolike count --country US --param social=github ``` ``` {"error": "ValidationError", "message": "social.0: Input should be 'facebook', 'instagram', 'linkedin', 'pinterest', 'threads', 'tiktok', 'twitter', 'x', 'yelp', 'youtube', 'googleplay', 'applestore', 'amazon', 'vk', 'bluesky' or 'xing'", "status_code": null} ``` The parameter names accepted by each endpoint are listed in the [API reference](https://docs.discolike.com/api/endpoints/discover/). ### Whole requests and domain lists from files Two flags keep volume work inside a normal command line: - `--params-file form.json` on `discover`, `count`, `contacts search`, `contacts count`, `contacts discover`, and the `bulk` commands loads a JSON object keyed by API parameter names, the shape you get by copying an app form over. Precedence is file, then `--param`, then named flags. The file is validated against the request schema before anything is sent. - `--domains-file companies.csv` on `queries create-exclusion-list`, `contacts search`, `contacts count`, `contacts discover`, `contacts generate`, `discogen run`, `validate-icp`, and the `bulk` commands reads a CSV with a `domain` column (or plain text, one domain per line), lower-cases, strips `www.`, de-duplicates, and merges with any inline `--domain`. `discover --exclude-domains-file` does the same for `exclude_domain`, up to its cap of 100. ```json { "icp_text": "Independent marketing and advertising agencies serving mid-market brands", "category": ["ADVERTISING_AND_MARKETING"], "country": ["US"], "employee_range": "11,200", "relevance": "BALANCED", "min_similarity": 40, "exclude_leadgen": true } ``` ```bash discolike count --params-file form.json discolike discover --params-file form.json --max-records 100 --format json > round1.json ``` ## Managed prospecting `discolike prospecting start --brief "Find 100 software companies and 3 founders each" --idempotency-key software-1` drafts a plan. `prospecting wait RUN_ID` returns at `proposed`; review it and run `prospecting approve RUN_ID --plan-version VERSION`. Use `prospecting message` to steer or answer a question, `prospecting status` with event/message cursors to follow replies, and `prospecting list` for recent organization runs. Omitted quantity flags preserve brief inference. See the [command reference](https://docs.discolike.com/cli/reference/#prospecting) for exact flags and limits. ## Common workflows Task-shaped workflows that chain several commands together. Each recipe states the goal, the commands, the real output, and what to do next. For the full option list on any command, see [Command Reference](https://docs.discolike.com/cli/reference/). Every example assumes `DISCOLIKE_API_KEY` is set or `discolike auth login` has been run; see [Authentication](#authentication). ### Size a market before you spend credits `count` is free of record charges and takes the same filters as `discover`. Iterate on `count` until the number looks like a segment you would actually work, then run `discover` once. 1. Start broad and see how big the universe is. ```bash discolike count --category SAAS --country US ``` ```json { "count": 248818 } ``` 2. Add a size band. `--employee-range` takes `min,max`, with a comma, not a dash. ```bash discolike count --category SAAS --country US --employee-range 51,200 ``` ```json { "count": 9575 } ``` 3. Add an intent signal. `--phrase-match` requires the phrase to appear on the company website. ```bash discolike count --category SAAS --country US --employee-range 51,200 \ --phrase-match "SOC 2" ``` ```json { "count": 977 } ``` 4. Or filter on what they already run. `--tech-stack` takes the **vendor's domain**, not a product name: `hubspot.com`, not `hubspot`. ```bash discolike count --category SAAS --country US --employee-range 51,200 \ --tech-stack hubspot.com ``` ```json { "count": 5097 } ``` A bare product name is rejected with exit code 2: ```bash discolike count --category SAAS --tech-stack hubspot ``` ```json {"error": "ValidationError", "message": "Invalid Domain : hubspot", "status_code": 400} ``` Once the count is in a range you can act on, reuse the exact same flags on `discover` and add `--max-records`. See [Count](https://docs.discolike.com/api/endpoints/count/) for the full filter vocabulary. ### Build a target list from an ICP description and write it to CSV `--icp-prompt` takes plain language and derives filters from it. Combine it with hard filters to keep the result grounded. ```bash discolike discover \ --icp-prompt "developer tools startups selling to engineering teams" \ --country US \ --max-records 5 \ > targets.json ``` `discover` writes a top-level JSON array to stdout, one object per company: ```bash jq -r '.[] | [.domain, .name, .employees, .similarity] | @tsv' targets.json ``` ``` codecones.com CodeCones, Inc. 1-10 91.0 linearb.io LinearB 51-200 91.0 aionyxtech.com Aionyx Technologies Inc. 1-10 91.0 digitaljackalope.com Digital Jackalope LLC 1-10 91.0 ibute.tech ibute Technologies 1-10 90.0 ``` Turn it into a CSV your CRM will accept: ```bash jq -r '(["domain","name","employees"]), (.[] | [.domain, .name, .employees]) | @csv' \ targets.json > targets.csv ``` ``` "domain","name","employees" "codecones.com","CodeCones, Inc.","1-10" "linearb.io","LinearB","51-200" "aionyxtech.com","Aionyx Technologies Inc.","1-10" "digitaljackalope.com","Digital Jackalope LLC","1-10" "ibute.tech","ibute Technologies","1-10" ``` Each result carries `similarity`, `score`, `revenue_range`, `vendors`, and `keywords`. Pull whatever your downstream system needs out of the same JSON rather than making a second call. **Next:** feed `targets.csv` into [enrichment](#enrich-an-existing-csv-of-domains) or [contact discovery](#find-contacts-at-a-set-of-companies-then-get-their-emails). ### Enrich an existing CSV of domains `append` is **synchronous** and returns the enriched rows in the same call. Point it at a CSV, name the column that holds the domains, and pick one or more datasets. ```bash cat seed.csv ``` ``` domain stripe.com linear.app ``` ```bash discolike append seed.csv --dataset bizdata --domain-column domain ``` Returns a JSON array, one object per input row. Each row keeps `input:domain` so you can join back to the source file: ```json [ { "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": "California", "zip": "94080", "country": "US" }, "phones": ["+16504279276", "+18889262289"], "public_emails": ["sales@stripe.com", "..."], "..." }, "..." ] ``` To get flat CSV back instead of nested JSON, pass `--csv` **and** `--output`: ```bash discolike append seed.csv --dataset bizdata --domain-column domain \ --csv --output enriched.csv ``` ```json { "written": "enriched.csv", "bytes": 3530 } ``` The output columns are namespaced by dataset: ``` input:domain,bizdata:social_urls,bizdata:redirect_domain,bizdata:revenue_range,bizdata:confidence,... ``` Repeat `--dataset` to append several at once. See [Append](https://docs.discolike.com/api/endpoints/append/) for the dataset catalogue. ### Match a messy CRM export of company names to domains For a single name, `match` is synchronous. Use the address flags to disambiguate. ```bash discolike match "Stripe" --country US ``` ```json { "query": { "name": "Stripe", "country": "US", "state": null, "city": null, "zip": null, "phones": null }, "matches": [ { "domain": "stripe.com", "name": "Stripe", "status": { "status": "active", "confidence": 0.89 }, "score": 701, "address": { "city": "South San Francisco", "state": "California", "country": "US" }, "..." } ] } ``` Pull just the winner: ```bash discolike match "Stripe" --country US | jq -r '.matches[0] | {domain, name, score}' ``` ```json { "domain": "stripe.com", "name": "Stripe", "score": 701 } ``` For a whole CRM export, swap the positional name for `--file`. That switches `match` to an async bulk job: ```bash discolike match --file crm-accounts.csv --name-column company_name --wait --timeout 1800 ``` `--wait` streams `progress: N%` lines to **stderr** and prints the results to stdout when the job finishes, so `> out.json` captures only the payload. Without `--wait` you get the handle immediately and can walk away: ```bash discolike match --file crm-accounts.csv --name-column company_name ``` ```json { "task_id": "...", "task_family": "bulkmatch", "hint": "poll with: discolike discogen status {id} --family {family}" } ``` `--name-column` defaults to `name`. `--strict` / `--no-strict` and `--local-mode` / `--no-local-mode` are tri-state: leave them off entirely and the server default applies. The Python equivalent is `client.match.bulk(MatchBulkParams(name_column=...), file=...)`, which returns a `Job` you can `.wait()` on; see [match](https://docs.discolike.com/sdk/reference/#match) and [Job](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). ### Find contacts at a set of companies, then get their emails Size the contact pool first; `contacts count` is the cheap probe. ```bash discolike contacts count --domain stripe.com --seniority vp --has-email ``` ```json { "count": 56 } ``` Then pull the records. `--has-email` restricts results to contacts that carry an email address. Search results are unverified (`email_validated` is `null`), so verify addresses before sending to them. ```bash discolike contacts search --domain linearb.io --seniority vp --has-email --max-records 20 \ > contacts.json jq -r '.[] | [.name, .title, .email] | @tsv' contacts.json ``` ``` Lior Shlezinger VP Finance lior@linearb.io Jessica Miller VP of People & Culture jess@linearb.io Ilan Rado Vice President of Product Management ilan.rado@linearb.io Craig Zelley VP, Revenue Operations craig.zelley@linearb.io ``` Repeat `--domain` to cover a whole account list. To drive it from a file of domains: ```bash xargs -a domains.txt -I{} discolike contacts search --domain {} --has-email --max-records 20 \ > contacts.ndjson ``` When the index has no email for a person, find one with `discolike email`. The batch finder reads a CSV with exactly three columns: ``` first_name,last_name,domain Patrick,Collison,stripe.com Ada,Lovelace,acme.com ``` ```bash discolike email find-batch --contacts-file people.csv --wait --timeout 900 > emails.json ``` A batch holds at most 500 contacts, so split longer lists. Mix in one-off people with repeatable `--contact "First,Last,domain.com"` flags. Only proven addresses bill; catch-all domains and pattern guesses come back free. For a single person, skip the CSV: ```bash discolike email find Patrick Collison stripe.com --wait --timeout 120 ``` ```json { "first_name": "patrick", "last_name": "collison", "domain": "stripe.com", "status": "catch_all_pattern", "result": { "email": "patrick.collison@stripe.com", "pattern": "first.last", "tier": 1, "smtp_code": 0, "valid": false }, "is_catch_all": true, "mx_host": "aspmx3.googlemail.com", "provider": "Everything Else", "attempts": 0, "duration_ms": 1473, "error": null } ``` Drop `--wait` on either command to get an ID back immediately and poll later with `discolike email job ` or `discolike email results `. See [email commands](https://docs.discolike.com/cli/reference/#email), [email](https://docs.discolike.com/sdk/reference/#email), and the [Email Find endpoint](https://docs.discolike.com/api/endpoints/email-find/). ### Run a DiscoGen research job and poll it to completion DiscoGen answers a free-text research question across a set of domains. It is always an async job. Fire and forget, then poll on your own schedule: ```bash discolike discogen run \ --query "Which of these companies has publicly announced a SOC 2 Type II certification?" \ --domain stripe.com --domain linear.app --domain vercel.com ``` ```json { "task_id": "...", "task_family": "discogen", "hint": "poll with: discolike discogen status {id} --family {family}" } ``` ```bash discolike discogen status discolike discogen cancel ``` Or block in one call: ```bash discolike discogen run \ --query "Which of these companies has publicly announced a SOC 2 Type II certification?" \ --domain stripe.com --domain linear.app \ --wait --timeout 1800 ``` `--timeout` defaults to `900.0` seconds. A client-side timeout does **not** cancel the job; it keeps running server-side, and you can pick it back up with `discogen status`. `discogen status` and `discogen cancel` are the polling front-end for **every** async family, not just DiscoGen. Pass `--family` to reach the others: | Started by | `--family` | |---|---| | `discolike discogen run`, `discogen run-personas`, `validate-icp`, `contacts generate` | `discogen` (default) | | `discolike match --file` | `bulkmatch` | | `discolike contacts bulk-match` | `contactmatch` | | `discolike segment` | `segment` | ```bash discolike discogen status --family bulkmatch ``` An unknown family exits 2. See [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/) for the research parameters and [Job](https://docs.discolike.com/sdk/reference/#job-and-asyncjob) for the Python equivalent. ### Stop re-surfacing companies you already own Save your existing customers as an exclusion list once, then attach it to every `discover` run. `--domains-file` reads the list from a CSV with a `domain` column or a plain text file with one domain per line, so nothing has to fit on the command line: ```bash discolike queries create-exclusion-list \ --name "Existing customers" \ --domains-file customers.txt \ --tag customers ``` ```json { "query_id": "a20af20c-0e3b-4dbf-bf08-2ade89993fc6", "query_name": "Existing customers", "action": null, "domain_count": 20, "persona_id_count": 0, "row_count": null, "tags": ["customers"], "message": null } ``` Now pass the id to `discover`. The same query, before and after: ```bash discolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5 ``` ``` codecones.com CodeCones, Inc. linearb.io LinearB aionyxtech.com Aionyx Technologies Inc. digitaljackalope.com Digital Jackalope LLC ibute.tech ibute Technologies ``` ```bash discolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5 \ --exclusion-query-id a20af20c-0e3b-4dbf-bf08-2ade89993fc6 ``` ``` codecones.com CodeCones, Inc. aionyxtech.com Aionyx Technologies Inc. digitaljackalope.com Digital Jackalope LLC ibute.tech ibute Technologies allstacks.com Allstacks, Inc. ``` `linearb.io` is gone and the slot is backfilled. `--exclusion-query-id` is repeatable, so you can stack a customers list, a churned-accounts list, and an open-opportunities list on one call. For ad-hoc one-offs use `--exclude-domain` instead, which is also repeatable. Keep the list current by updating it as you close deals, and find it later by tag: ```bash discolike queries list --tag customers discolike queries update --name "Existing customers (2026)" --tag customers ``` ### Save a round of results and reuse it Persist the domains you already surfaced so the next run can exclude them, or so a teammate can pick up the same set. ```bash discolike discover --icp-prompt "developer tools startups selling to engineering teams" \ --country US --max-records 5 \ | jq -r '(["domain","name"]), (.[] | [.domain, .name]) | @csv' > round1.csv discolike queries save-results \ --input round1.csv \ --name "Round 1 — devtools US" \ --action discover \ --domain-column domain \ --tag round1 ``` ```json { "query_id": "a848a58a-d0f3-4f5a-9003-768de7460e89", "query_name": "Round 1 — devtools US", "action": "thin_discover", "domain_count": 5, "persona_id_count": 0, "row_count": 5, "tags": ["round1"], "message": null } ``` `--input` accepts `.csv` (header row, every value stays a string) or `.json` (a list of row objects); the extension decides. A missing file or malformed JSON exits 2. Unlike `create-exclusion-list`, `save-results` has **no 20-domain minimum**; five rows saved fine above. Both produce a `query_id` usable as `--exclusion-query-id`, so the next round skips what you already pulled: ```bash discolike discover --icp-prompt "..." --country US --max-records 5 \ --exclusion-query-id a848a58a-d0f3-4f5a-9003-768de7460e89 ``` Clean up when a list is stale: ```bash discolike queries delete a848a58a-d0f3-4f5a-9003-768de7460e89 ``` ```json { "deleted": "a848a58a-d0f3-4f5a-9003-768de7460e89" } ``` ### Pull past 10,000 companies and contacts at all of them A single `discover` returns at most 10,000 companies. `discolike bulk` runs the loop that gets past that, saves each page as an exclusion list for the next one, then pulls contacts for every domain in the result, all with checkpoint and resume so a dropped connection costs nothing. Start from the count, which is free and is the ceiling for the run: ```bash discolike count --params-file form.json ``` Walk the index. Each page lands in the CSV as it arrives; rerunning the same command resumes from the file: ```bash discolike bulk companies --params-file form.json --max-companies 50000 \ --run-name agencies --out companies.csv ``` Size the contact pull before paying for it. `contacts_capped` is the most the pull can return; two to three usable emails per company is a normal yield at 5 to 10 per company: ```bash discolike bulk estimate --domains-file companies.csv --per-company 10 \ --summary "growth marketing lead" --negate-summary "controller, accountant, bookkeeping" ``` Pull the contacts with the same filters. Slices are checkpointed in `contacts.csv.checkpoint`; a rerun skips the finished ones and never re-bills a record pulled in the last 90 days: ```bash discolike bulk contacts --domains-file companies.csv --per-company 10 \ --summary "growth marketing lead" --negate-summary "controller, accountant, bookkeeping" \ --out contacts.csv ``` From here `companies.csv` feeds `discogen run --domains-file`, `validate-icp --domains-file`, and `contacts generate --domains-file` directly. The full option list and the exact mechanics are in the [reference](https://docs.discolike.com/cli/reference/#bulk). ### Wire the CLI into a shell script Three properties make the CLI scriptable: results go to **stdout**, errors and progress go to **stderr**, and the exit code tells you which failure you hit. #### Exit codes | Code | Meaning | |---|---| | 0 | Success | | 1 | Server error, any unmapped failure, or a failed browser login (`LoginError`) | | 2 | Validation error, client-side (bad enum value, number out of range, missing required option) or server-side (400/422), or a usage error: bad flag, unknown command | | 3 | Authentication (401) or plan access (402/403) | | 4 | Rate limited (429) | | 5 | Connection failure | | 6 | Not found (404) | Verified: ```bash discolike count --category SAAS >/dev/null 2>&1; echo $? # 0 discolike count --nope >/dev/null 2>&1; echo $? # 2 discolike --api-key bogus account usage >/dev/null 2>&1; echo $? # 3 discolike discogen status not-a-real-task >/dev/null 2>&1; echo $? # 6 ``` Errors are a single line of JSON on stderr, never mixed into stdout: ```json {"error": "AuthenticationError", "message": "Invalid API Key or Session", "status_code": 401} ``` A `RateLimitError` adds `retry_after` when the server sent a numeric `Retry-After` header, which is the hook for a backoff loop. #### A script that branches on the exit code ```bash #!/usr/bin/env bash set -uo pipefail code=0 out=$(discolike count --category SAAS --country US --employee-range 51,200 2>err.json) || code=$? case $code in 0) ;; 3) echo "auth problem: $(jq -r .message err.json)" >&2; exit 1 ;; 4) echo "rate limited, retry in $(jq -r '.retry_after // 60' err.json)s" >&2; exit 1 ;; *) echo "count failed ($code): $(jq -r .message err.json)" >&2; exit "$code" ;; esac count=$(jq -r .count <<<"$out") if (( count > 50000 )); then echo "segment too broad ($count) — tighten the filters" >&2 exit 1 fi discolike discover --category SAAS --country US --employee-range 51,200 \ --max-records 100 > targets.json ``` #### Format is TTY-dependent `--format` is only honoured as the literal string `table`. Everything else, including `--format json`, produces JSON. With no `--format`, the CLI emits a rich table when stdout is a terminal and JSON when it is a pipe or a file. In a script you therefore get JSON automatically, but pass `--format json` anyway if the script might ever run attached to a TTY. See [Output and scripting](#output-and-scripting) for the table rules. #### Resuming a long job by task id Start the job, keep the id, and poll from anywhere: ```bash task_id=$(discolike match --file crm-accounts.csv --name-column company_name \ | jq -r .task_id) echo "$task_id" > .last-task while true; do state=$(discolike discogen status "$task_id" --family bulkmatch | jq -r .status) echo "state: $state" >&2 case "$state" in completed|failed|cancelled) break ;; esac sleep 30 done [ "$state" = "completed" ] || { echo "job $task_id ended as $state" >&2; exit 1; } discolike discogen status "$task_id" --family bulkmatch > results.json ``` A status response carries `status` and a `progress` percentage. The three terminal states are `completed`, `failed`, and `cancelled`; break on all three or the loop never exits. The task id stays valid independently of the process that started it, so a `--wait` that timed out client-side is not lost. Re-poll with `discogen status` and the correct `--family`. ### When to reach for the SDK instead The CLI and the Python SDK cover the same API. Reach for Python when you need: | Task | In Python | |---|---| | Fanning out across hundreds of domains | `AsyncDiscolike` runs requests concurrently; see [Async](https://docs.discolike.com/sdk/#async). For the past-10k loop and per-domain contact pulls, `discolike bulk` already does it in the CLI. | | Custom retry or backoff policy | Configure `max_retries` and `timeout` on the client; see [The client](https://docs.discolike.com/sdk/#the-client). | | Joining several jobs together | `Job.wait()` and typed result models, instead of parsing JSON in bash; see [Job](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). | Start at [Getting Started](https://docs.discolike.com/sdk/) for the library equivalent of every command on this page. ## Shell completion Typer's built-in completion support is enabled. Install it for the shell you are running: ```bash discolike --install-completion ``` To inspect the script first, or to install it somewhere of your own choosing: ```bash discolike --show-completion ``` Both flags detect the current shell from the environment and fail with `Shell not supported.` if they cannot identify it. Run them from an interactive shell rather than from a script. Completion applies to the root command and its subcommands; there are no per-group completion flags. ## Command Reference Every `discolike` command, grouped the way the CLI groups them. Each entry lists the exact usage line, every option, and a runnable example. New here? Start with [Getting Started](https://docs.discolike.com/cli/) for install, authentication, and output formats. ## account Account usage and quota. ### account usage Month-to-date usage: requests, records, and spend. ``` discolike account usage [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike account usage ``` ```json { "requests_mtd": null, "records_mtd": null, "spend_mtd": null, "account_status": "active", "month_to_date_requests": 14, "month_to_date_records": 11794, "month_to_date_spend": 24.99, "max_spend": "unlimited", "total_available_spend": "unlimited", "carryover_credits": 0.0, "top_up_credits": 0.0, "usage_summary": { "2026-08": [ { "access_id": "••••••••isco", "description": "[API] Internal key", "requests": 13, "total_records": 11744, "monthly_spend": 24.79 } ], ... }, ... } ``` REST endpoint: [Usage API](https://docs.discolike.com/api/endpoints/usage/). SDK equivalent: [`client.account.usage()`](https://docs.discolike.com/sdk/reference/#accountusage). ## append Enrich a CSV of domains with DiscoLike datasets. This command is synchronous and returns the enriched rows directly. ``` discolike append [OPTIONS] FILE ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `FILE` (argument) | path | required | CSV of domains to enrich. | | `--dataset` | str | required | Dataset to append (repeatable): `bizdata`, `redirects`, `domain_status`, `growth`, `vendors`. Omitting it exits 2 before any request. | | `--domain-column` | str | none | Column in the CSV that holds domains. | | `--csv` / `--no-csv` | bool | unset | Request the enriched rows as CSV (written via `--output`). | | `--output` | path | none | File to write a CSV response to. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--query-id` | str | none | Saved query ID whose domains are included alongside the file/--domain ones (repeatable). | Pass `--output` whenever the API returns CSV bytes; without it the command exits 2 with `--output is required when the response is CSV bytes`. With `--output` it writes the bytes verbatim and prints `{"written": "", "bytes": }`. ```bash discolike append domains.csv --dataset bizdata --dataset growth ``` ```bash discolike append domains.csv --domain-column website --csv --output enriched.csv ``` REST endpoint: [Append API](https://docs.discolike.com/api/endpoints/append/). SDK equivalent: [`client.append()`](https://docs.discolike.com/sdk/reference/#append). ## auth Manage credentials: log in (browser or API key), check status, log out. `auth login` and `auth status` honour the root `--base-url`: `auth login` logs in to that host's authorization server and `auth status` verifies against it. A root `--api-key` passed explicitly on the command line switches `auth login` to the API-key flow with that key, and is the key `auth status` reports as `"source": "option"`. Credential resolution, consent reuse, and the config file are covered in [Getting Started](https://docs.discolike.com/cli/#authentication). ### auth login Log in via the browser (OAuth) or with an API key, verify, and save to the local config file. ``` discolike auth login [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--api-key` | str | none | Log in with an API key instead of the browser. Create one at https://app.discolike.com/account/management/keys. | | `--method` | str | `oauth` | `oauth` (browser login, default) or `api_key` (prompts for a key unless `--api-key` is given). Any other value is a usage error, exit 2. | | `--no-browser` | flag | off | Print the login URL instead of opening a browser. | | `--port` | int | `0` | Fixed loopback port for the browser redirect (default: random; use with SSH). | **Browser flow** (the default). The CLI fetches the authorization server's metadata from the API host, registers itself as a public PKCE client or reuses the registration saved from an earlier login, starts a listener on `http://127.0.0.1:/callback`, and prints the authorization URL to stderr before opening it. If no browser can be opened it says so and leaves the URL for you to open by hand. It then waits up to 180 seconds for the redirect, exchanges the code for tokens, verifies the session with a usage call, and saves it. On success the payload goes to **stderr**, not stdout: ```json {"logged_in": true, "method": "oauth", "expires_at": "2026-08-28T20:15:00+00:00"} ``` ```bash discolike auth login ``` On a remote machine, pin the port, forward it, and open the printed URL in a local browser: ```bash ssh -L 51234:127.0.0.1:51234 build-host discolike auth login --no-browser --port 51234 ``` **API-key flow.** `--api-key KEY` uses that key; `--method api_key` prompts for one with hidden input. The key is verified with a usage call before anything is written, and a bad key exits 3 and writes nothing. The success payload is `{"logged_in": true, "source": "api_key"}` on stderr. ```bash discolike auth login --api-key dk-live-... ``` ```bash discolike auth login --method api_key ``` Failures of the browser flow itself exit 1 with a single JSON line on stderr: | `message` | Cause | | --- | --- | | `Timed out after 180s waiting for the browser login` | No redirect arrived within the wait | | `Authorization failed: ` | The authorization server redirected back with an error, such as `access_denied` when consent was refused | | `Invalid OAuth callback (state mismatch or missing code)` | The redirect did not carry the expected `state` and `code` | | `Token exchange failed: ` | The code could not be exchanged for tokens | ```json {"error": "LoginError", "message": "Authorization failed: access_denied"} ``` A saved client registration the server no longer accepts (`invalid_client` or `unauthorized_client`) is discarded and replaced automatically, with the browser flow run once more; only a second rejection surfaces as a `LoginError`. Errors from the authorization server outside the browser round trip, such as unreachable metadata or a rejected registration, are `AuthenticationError` and exit 3 like any other API error. ### auth status Show which credential is in use (option, env, or config) and verify it against the API. No options. ``` discolike auth status ``` | Field | Present | Value | | --- | --- | --- | | `source` | always | `option` for a root `--api-key`, `env` for `DISCOLIKE_API_KEY`, `config` for the config file | | `method` | always | `oauth` or `api_key` | | `api_key` | API key only | The key masked to its last four characters | | `expires_at` | OAuth only | Access-token expiry as an ISO 8601 UTC timestamp | | `expired` | OAuth only | Whether the access token is past `expires_at`. The session is still refreshed on the next command, so this can be `true` while `valid` is `true` | | `valid` | always | `true`; an invalid credential raises instead | The option is checked first, then the environment variable, then the config file. With no credential anywhere, it exits 3. ```bash discolike auth status ``` ```json { "source": "config", "method": "oauth", "expires_at": "2026-08-28T20:15:00+00:00", "expired": false, "valid": true } ``` ```json { "source": "env", "method": "api_key", "api_key": "…isco", "valid": true } ``` ### auth logout Delete saved credentials from the local config file (the registered OAuth client is kept). No options. Idempotent - logging out twice is not an error. ``` discolike auth logout ``` ```bash discolike auth logout ``` Prints `{"logged_out": true}`. The OAuth session or API key is removed; the `oauth_client` registration stays so the next browser login does not ask for consent again. ## bulk Volume pulls with checkpoint and resume: walk the company index past the 10,000-per-search ceiling, size a contact pull for free, then pull N contacts per company into one CSV. Filters come from `--params-file`, `--param`, and the common flags; the paging parameters are managed by the command and a supplied value is dropped with a note on stderr. Every `bulk` command validates the request against the SDK model before the first billable call, keeps one request in flight under `--rate-limit`, retries on 429 and 5xx above the SDK's own retries, prints progress to stderr, and prints one JSON summary to stdout. Rate limits are per plan: `/discover` and `/contacts` allow 5 per minute on Starter, 10 on Pro, 15 on Team, 25 on Company, and 50 on Enterprise; `/contacts/count` allows six times that. ### bulk companies Walk the company index past 10,000 results, one exclusion list per page, appending to a CSV. ``` discolike bulk companies [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-prompt` | str | none | Natural-language ideal customer profile description. | | `--domain` | str | none | Seed domain for lookalike matching (repeatable). | | `--phrase-match` | str | none | Phrase the company website must contain (repeatable). | | `--negate-phrase-match` | str | none | Negate the `--phrase-match` filter (repeatable). | | `--category` | str | none | Industry category filter (repeatable). | | `--country` | str | none | ISO country code filter (repeatable). | | `--state` | str | none | State or region filter (repeatable). | | `--employee-range` | str | none | Employee count range filter. | | `--revenue-range` | str | none | Revenue range filter. | | `--relevance` | str | none | Where a lookalike list stops: MAX_RELEVANCE (shortest, tightest), BALANCED (the app's default), MAX_COVERAGE (longest, more borderline companies). Omit for no automatic stop. | | `--variance` | str | none | Deprecated. LOW, MID_LOW, MEDIUM act as MAX_RELEVANCE; MID_HIGH, HIGH as BALANCED; UNRESTRICTED as no stop; `--relevance` wins when both are sent. Use `--relevance`. | | `--exclusion-query-id` | str | none | Saved query ID whose results are excluded (repeatable). Also the way to reuse the lists a previous run created. | | `--param` | str | none | Extra API parameter as `KEY=VALUE` (comma-separates into a list); see the endpoint reference (repeatable). | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | | `--max-companies` | int | `10000` | Stop once this many companies are in `--out`. | | `--page-size` | int | `10000` | Companies per `discover` call (20 to 10000). | | `--run-name` | str | `bulk` | Prefix for the exclusion lists created per page (`-round-N`). | | `--out` | path | `companies.csv` | CSV to append each page to; resumes if present. | | `--overwrite` | flag | off | Ignore an existing `--out` and start clean instead of resuming. | | `--rate-limit` | int | `10` | Calls per minute to stay under on `/discover`. | Mechanics: each page is a `discover` call at `--page-size`; its net-new domains are appended to the CSV (`domain`, `name`, `country`, `employees`, `similarity`) and saved as an exclusion list named `-round-N`, which is passed as `exclusion_query_id` on the next page (a tail shorter than 20 domains rides on `exclude_domain` instead, since saved lists need at least 20). The loop stops when a page returns nothing net-new, when a page comes back shorter than requested, or at `--max-companies`. Rerunning with the same `--out` reads the CSV back and rebuilds the exclusion set in 10,000-domain lists before continuing; pass the original ids as `--exclusion-query-id` to reuse them instead. Reach is bounded by the account's exclusion capacity of 250,000 domains; the command warns there, and the fix is to slice the ICP (by state, employee band, sub-vertical) into separate runs. `max_records`, `offset`, `exclude_domain`, and `exclusion_query_id` inside `--params-file` or `--param` are managed by the command and dropped with a note. ```bash discolike bulk companies --params-file form.json --max-companies 50000 \ --run-name agencies --out companies.csv ``` ``` round 1: +10,000 net-new (10,000 total) round 2: +10,000 net-new (20,000 total) ... ``` ```json { "companies": 43210, "new": 43210, "rounds": 5, "exclusion_query_ids": ["…", "…", "…", "…"], "out": "companies.csv" } ``` REST endpoint: [Discover API](https://docs.discolike.com/api/endpoints/discover/) and [Queries API](https://docs.discolike.com/api/endpoints/queries/). SDK equivalent: `client.discover()` in a loop with `client.queries.create_exclusion_list()`. ### bulk estimate Size a contact pull for free: matching contacts across the domain list, and the cap at `--per-company`. ``` discolike bulk estimate [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--domains-file` | path | required | CSV with a `domain` column, or one domain per line. | | `--per-company` | int | `10` | Contacts per company the pull would ask for (1 to 100). | | `--icp-prompt` | str | none | Natural-language ICP prompt used to derive contact filters. | | `--summary` | str | none | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. Ranks who comes back per company. | | `--negate-summary` | str | none | Exclude contacts whose profile summary contains every word of any one term, in any order. | | `--seniority` | str | none | Filter by seniority level (repeatable). | | `--negate-seniority` | str | none | Exclude seniority levels (repeatable). | | `--department` | str | none | Filter by department (repeatable). | | `--negate-department` | str | none | Exclude departments (repeatable). | | `--title` | str | none | Filter by job title (repeatable). | | `--negate-title` | str | none | Exclude job titles (repeatable). | | `--person-country` | str | none | Filter by contact country (repeatable). | | `--person-state` | str | none | Filter by contact state/region (repeatable). | | `--has-email` / `--no-has-email` | bool | on | Only contacts with an email address. | | `--exclusion-query-id` | str | none | Saved query ID whose results are excluded (repeatable). | | `--param` | str | none | Extra API parameter as `KEY=VALUE` (comma-separates into a list); see the endpoint reference (repeatable). | | `--params-file` | path | none | JSON object of API parameter names; `--param` and named flags override its values. | | `--rate-limit` | int | `30` | Calls per minute on the free `/contacts/count`. | Runs `contacts count` over the list in 1,000-domain slices with the same filters `bulk contacts` will use and sums the result. `contacts_capped` is the most the pull can return at `--per-company`; the real yield is usually lower, two to three usable emails per company is a normal outcome at 5 to 10 per company once ranking and `--negate-summary` apply. ```bash discolike bulk estimate --domains-file companies.csv --per-company 10 \ --summary "growth marketing lead" --negate-summary "controller, accountant, bookkeeping" ``` ```json { "companies": 43210, "contacts_available": 118004, "contacts_capped": 118004, "per_company": 10 } ``` REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.count()`. ### bulk contacts Pull up to `--per-company` contacts for every domain in the file into one CSV, resumable by slice. ``` discolike bulk contacts [OPTIONS] ``` Uses the same filters as `bulk estimate`, with output and resume controls: | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--domains-file` | path | required | CSV with a `domain` column, or one domain per line. | | `--per-company` | int | `10` | Contacts per company the pull would ask for (1 to 100). | | `--icp-prompt` | str | none | Natural-language ICP prompt used to derive contact filters. | | `--summary` | str | none | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. Ranks who comes back per company. | | `--negate-summary` | str | none | Exclude contacts whose profile summary contains every word of any one term, in any order. | | `--seniority` | str | none | Filter by seniority level (repeatable). | | `--negate-seniority` | str | none | Exclude seniority levels (repeatable). | | `--department` | str | none | Filter by department (repeatable). | | `--negate-department` | str | none | Exclude departments (repeatable). | | `--title` | str | none | Filter by job title (repeatable). | | `--negate-title` | str | none | Exclude job titles (repeatable). | | `--person-country` | str | none | Filter by contact country (repeatable). | | `--person-state` | str | none | Filter by contact state/region (repeatable). | | `--has-email` / `--no-has-email` | bool | on | Only contacts with an email address. | | `--exclusion-query-id` | str | none | Saved query ID whose results are excluded (repeatable). | | `--param` | str | none | Extra API parameter as `KEY=VALUE` (comma-separates into a list); see the endpoint reference (repeatable). | | `--params-file` | path | none | JSON object of API parameter names; `--param` and named flags override its values. | | `--out` | path | `contacts.csv` | CSV to append to; `.checkpoint` tracks finished slices. | | `--overwrite` | flag | off | Ignore an existing `--out` and its checkpoint and start clean. | | `--rate-limit` | int | `10` | Calls per minute to stay under on `/contacts/discover`. | Mechanics: `/contacts/discover` ignores `offset` when `results_by_company` is set, so the command paginates by slicing the domain list at `10000 / per-company` domains per call, with `results_by_company` at `--per-company` and `max_records` at the slice's cap. Each response is flattened to one row per contact (`persona_id`, `first_name`, `last_name`, `name`, `title`, `seniority`, `department`, `email`, `email_validated`, `phone`, `linkedin`, `connections`, `domain`, `company_name`, `country`, `state`, `industry`, `employees`, `revenue_range`, `jobstart_date`), de-duplicated on `persona_id` against the run and the rows already in the CSV, and appended; the slice index is then written to `.checkpoint`. A rerun skips finished slices. Billing is net-new only with a 90-day cache, so a rerun after a crash does not re-charge for records already pulled. `domain`, `max_records`, `offset`, `results_by_company`, and `max_companies` inside `--params-file` or `--param` are managed by the command and dropped with a note. ```bash discolike bulk contacts --domains-file companies.csv --per-company 10 \ --summary "growth marketing lead" --negate-summary "controller, accountant, bookkeeping" \ --out contacts.csv ``` ``` 43,210 companies | 44 slice(s) of 1000 | 44 to run slice 1/44: 2,391 contacts (2,391 this run) slice 2/44: 2,402 contacts (4,793 this run) ... ``` ```json { "companies": 43210, "slices": 44, "slices_run": 44, "contacts": 104388, "out": "contacts.csv", "checkpoint": "contacts.csv.checkpoint" } ``` REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.discover()` per slice. ## company Company profiles by domain: firmographics, scores, growth, redirects, vendors, subsidiaries, and public links. Every subcommand takes a required `DOMAIN` argument and is synchronous. SDK equivalents for this whole group live on [`client.companies`](https://docs.discolike.com/sdk/reference/#companies). ### company data Full company profile (firmographics) for a domain. ``` discolike company data [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company data stripe.com ``` ```json { "domain": "stripe.com", "name": "Stripe", "status": { "status": "active", "confidence": 0.89 }, "score": 701, "start_date": "2011-01-04", "end_date": null, "address": { "street": "354 Oyster Point Blvd", "city": "South San Francisco", "state": "California", "zip": "94080", "country": "US" }, "phones": [ "+16504279276", "+18889262289" ], ... } ``` REST endpoint: [BizData API](https://docs.discolike.com/api/endpoints/bizdata/). SDK: `client.companies.data()`. ### company score Company score for a domain. ``` discolike company score [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company score stripe.com ``` ```json { "domain": "stripe.com", "score": 701, "parameters": { "base_score": 688.0, "recency_multiplier": 1.0, "growth_boost": 12.06, "lookback_360": 476, "lookback_720": 291, "expiration_penalty": 1.0 }, "first_event": "2011-01-04" } ``` REST endpoint: [Score API](https://docs.discolike.com/api/endpoints/score/). SDK: `client.companies.score()`. ### company growth Growth signals for a domain. ``` discolike company growth [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company growth stripe.com ``` REST endpoint: [Growth API](https://docs.discolike.com/api/endpoints/growth/). SDK: `client.companies.growth()`. ### company redirects Domain redirects for a company domain. ``` discolike company redirects [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--match` | str | none | Domain match mode, e.g. loose. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company redirects stripe.com --match loose ``` REST endpoint: [Redirects API](https://docs.discolike.com/api/endpoints/redirects/). SDK: `client.companies.redirects()`. ### company vendors Vendors associated with a company domain. ``` discolike company vendors [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--match` | str | none | Domain match mode, e.g. loose. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company vendors stripe.com ``` REST endpoint: [Vendors API](https://docs.discolike.com/api/endpoints/vendors/). SDK: `client.companies.vendors()`. ### company subsidiaries Subsidiaries of a company domain. ``` discolike company subsidiaries [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--match` | str | none | Domain match mode, e.g. loose. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company subsidiaries stripe.com --match loose ``` REST endpoint: [Subsidiaries API](https://docs.discolike.com/api/endpoints/subsidiaries/). SDK: `client.companies.subsidiaries()`. ### company public-links Public profile links for a domain from a given source. `--source` is required. ``` discolike company public-links [OPTIONS] DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `DOMAIN` (argument) | str | required | Company domain, e.g. stripe.com. | | `--source` | str | required | Public link source, e.g. crunchbase. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike company public-links stripe.com --source crunchbase ``` Omitting `--source` is a usage error and exits 2. REST endpoint: [PublicLink API](https://docs.discolike.com/api/endpoints/publiclink/). SDK: `client.companies.public_links()`. ## contacts Find contacts: search and count by filters, look up or match individuals, and run bulk/generative discovery jobs. SDK equivalents live on [`client.contacts`](https://docs.discolike.com/sdk/reference/#contacts). `search`, `count` and `discover` share one filter block, repeated in full in each table below. ### contacts search Search contacts matching the given filters. ``` discolike contacts search [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-prompt` | str | none | Natural-language ICP prompt used to derive contact filters. | | `--seniority` | str | none | Filter by seniority level (repeatable). | | `--negate-seniority` | str | none | Exclude seniority levels (repeatable). | | `--department` | str | none | Filter by department (repeatable). | | `--negate-department` | str | none | Exclude departments (repeatable). | | `--title` | str | none | Filter by job title (repeatable). | | `--negate-title` | str | none | Exclude job titles (repeatable). | | `--domain` | str | none | Filter by company domain (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. | | `--person-country` | str | none | Filter by contact country (repeatable). | | `--negate-person-country` | str | none | Exclude contact countries (repeatable). | | `--filter-industry` | str | none | Filter by company industry (repeatable). | | `--negate-filter-industry` | str | none | Exclude company industries (repeatable). | | `--filter-country` | str | none | Filter by company country (repeatable). | | `--negate-filter-country` | str | none | Exclude company countries (repeatable). | | `--employee-range` | str | none | Company employee range, e.g. 50-200. | | `--has-email` / `--no-has-email` | bool | unset | Only contacts with (or without) an email address. | | `--jobstart-date` | str | none | Job start date filter: min date or 'min,max' range, e.g. 2025-01-01 or 2025-01-01,2025-06-30. | | `--max-records` | int | none | Maximum number of contacts to return. | | `--offset` | int | none | Number of records to skip for pagination. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--param` | str | none | Extra key=value query parameter forwarded to the SDK (repeatable). | | `--name` | str | none | Filter by contact name (partial match supported). | | `--summary` | str | none | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. | | `--negate-summary` | str | none | Exclude contacts whose profile summary contains every word of any one term, in any order. | | `--skills` | str | none | Filter by skill (repeatable). | | `--person-state` | str | none | Filter by contact state/region (repeatable). | | `--persona-id` | int | none | Filter by persona ID (repeatable). | | `--filter-state` | str | none | Filter by company state/region (repeatable). | | `--negate-filter-state` | str | none | Exclude contacts at companies in this state (repeatable). | | `--email-validated` | bool | none | Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead. | | `--has-phone` | bool | none | Only contacts with (or without) a phone number. | | `--has-mobile` | bool | none | Only contacts with (or without) a mobile phone number. | | `--has-linkedin` | bool | none | Only contacts with (or without) a LinkedIn profile. | | `--min-connections` | int | none | Minimum LinkedIn connections required. | | `--inclusion-query-id` | str | none | Include only contacts from companies in this saved query (repeatable). | | `--exclusion-query-id` | str | none | Exclude contacts from companies in this saved query (repeatable). | | `--max-companies` | int | none | Maximum number of enriched companies to return; cannot be combined with --max-records. | | `--results-by-company` | int | none | Maximum contacts per company domain (default 5; 0 removes the cap). | | `--include-search-contacts` | bool | none | Include contacts from the search index (broader coverage). | | `--consensus` | int | none | Number of query vectors to combine for consensus search. | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | ```bash discolike contacts search --seniority vp --department marketing \ --domain stripe.com --has-email --max-records 5 ``` `--icp-text` is not accepted here; it exists only on `contacts generate`. REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.search()`. ### contacts count Count contacts matching the given filters. Same filter block as `search`, without `--max-records` and `--offset`. ``` discolike contacts count [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-prompt` | str | none | Natural-language ICP prompt used to derive contact filters. | | `--seniority` | str | none | Filter by seniority level (repeatable). | | `--negate-seniority` | str | none | Exclude seniority levels (repeatable). | | `--department` | str | none | Filter by department (repeatable). | | `--negate-department` | str | none | Exclude departments (repeatable). | | `--title` | str | none | Filter by job title (repeatable). | | `--negate-title` | str | none | Exclude job titles (repeatable). | | `--domain` | str | none | Filter by company domain (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. | | `--person-country` | str | none | Filter by contact country (repeatable). | | `--negate-person-country` | str | none | Exclude contact countries (repeatable). | | `--filter-industry` | str | none | Filter by company industry (repeatable). | | `--negate-filter-industry` | str | none | Exclude company industries (repeatable). | | `--filter-country` | str | none | Filter by company country (repeatable). | | `--negate-filter-country` | str | none | Exclude company countries (repeatable). | | `--employee-range` | str | none | Company employee range, e.g. 50-200. | | `--has-email` / `--no-has-email` | bool | unset | Only contacts with (or without) an email address. | | `--jobstart-date` | str | none | Job start date filter: min date or 'min,max' range, e.g. 2025-01-01 or 2025-01-01,2025-06-30. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--param` | str | none | Extra key=value query parameter forwarded to the SDK (repeatable). | | `--name` | str | none | Filter by contact name (partial match supported). | | `--summary` | str | none | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. | | `--negate-summary` | str | none | Exclude contacts whose profile summary contains every word of any one term, in any order. | | `--skills` | str | none | Filter by skill (repeatable). | | `--person-state` | str | none | Filter by contact state/region (repeatable). | | `--persona-id` | int | none | Filter by persona ID (repeatable). | | `--filter-state` | str | none | Filter by company state/region (repeatable). | | `--negate-filter-state` | str | none | Exclude contacts at companies in this state (repeatable). | | `--email-validated` | bool | none | Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead. | | `--has-phone` | bool | none | Only contacts with (or without) a phone number. | | `--has-mobile` | bool | none | Only contacts with (or without) a mobile phone number. | | `--has-linkedin` | bool | none | Only contacts with (or without) a LinkedIn profile. | | `--min-connections` | int | none | Minimum LinkedIn connections required. | | `--inclusion-query-id` | str | none | Include only contacts from companies in this saved query (repeatable). | | `--exclusion-query-id` | str | none | Exclude contacts from companies in this saved query (repeatable). | | `--max-records` | int | none | Maximum number of contacts to count (20-10000). | | `--max-companies` | int | none | Maximum number of enriched companies to return; cannot be combined with --max-records. | | `--offset` | int | none | Number of records to skip for pagination. | | `--results-by-company` | int | none | Maximum contacts per company domain (default 5; 0 removes the cap). | | `--include-search-contacts` | bool | none | Include contacts from the search index (broader coverage). | | `--consensus` | int | none | Number of query vectors to combine for consensus search. | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | ```bash discolike contacts count --seniority vp --domain stripe.com ``` ```json { "count": 56 } ``` REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.count()`. ### contacts lookup Look up a single contact by persona ID, LinkedIn URL, or email. ``` discolike contacts lookup [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--persona-id` | int | none | Look up by persona ID. | | `--linkedin` | str | none | Look up by LinkedIn profile URL. | | `--email` | str | none | Look up by email address. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | All three identifiers are optional, and you can pass several at once - the API decides which to use. ```bash discolike contacts lookup --email jane@example.com ``` REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.lookup()`. ### contacts match Match a person name to contact records. ``` discolike contacts match [OPTIONS] NAME ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `NAME` (argument) | str | required | Full name of the person to match. | | `--company-name` | str | none | Company name to narrow the match. | | `--domain` | str | none | Company domain to narrow the match. | | `--person-country` | str | none | Contact country to narrow the match. | | `--limit` | int | none | Maximum number of matches to return. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike contacts match "Jane Doe" --company-name "Acme Corp" --domain acme.com --limit 5 ``` REST endpoint: [Contact Match API](https://docs.discolike.com/api/endpoints/contact-match/). SDK: `client.contacts.match()`. ### contacts bulk-match Match many person queries in one async job. Task family `contactmatch`. ``` discolike contacts bulk-match [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--queries-file` | path | required | Path to a JSON file with an array of match query objects. | | `--enrich` / `--no-enrich` | bool | unset | Enable or disable enrichment of matched contacts. | | `--limit` | int | none | Maximum number of matches per query. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | The file must parse as JSON and contain a top-level array; anything else exits 2. ```bash discolike contacts bulk-match --queries-file queries.json --enrich --limit 5 --wait ``` Poll a non-waiting run with `discolike discogen status --family contactmatch`. REST endpoint: [Contact Bulk Match API](https://docs.discolike.com/api/endpoints/contact-bulk-match/). SDK: `client.contacts.bulk_match()`. ### contacts discover Discover contacts grouped by company for the given filters. ``` discolike contacts discover [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-prompt` | str | none | Natural-language ICP prompt used to derive contact filters. | | `--seniority` | str | none | Filter by seniority level (repeatable). | | `--negate-seniority` | str | none | Exclude seniority levels (repeatable). | | `--department` | str | none | Filter by department (repeatable). | | `--negate-department` | str | none | Exclude departments (repeatable). | | `--title` | str | none | Filter by job title (repeatable). | | `--negate-title` | str | none | Exclude job titles (repeatable). | | `--domain` | str | none | Filter by company domain (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. | | `--person-country` | str | none | Filter by contact country (repeatable). | | `--negate-person-country` | str | none | Exclude contact countries (repeatable). | | `--filter-industry` | str | none | Filter by company industry (repeatable). | | `--negate-filter-industry` | str | none | Exclude company industries (repeatable). | | `--filter-country` | str | none | Filter by company country (repeatable). | | `--negate-filter-country` | str | none | Exclude company countries (repeatable). | | `--employee-range` | str | none | Company employee range, e.g. 50-200. | | `--has-email` / `--no-has-email` | bool | unset | Only contacts with (or without) an email address. | | `--jobstart-date` | str | none | Job start date filter: min date or 'min,max' range, e.g. 2025-01-01 or 2025-01-01,2025-06-30. | | `--max-records` | int | none | Maximum number of contacts to return. | | `--offset` | int | none | Number of records to skip for pagination. | | `--results-by-company` | int | none | Maximum contacts returned per company. | | `--include-search-contacts` / `--no-include-search-contacts` | bool | unset | Include or exclude contacts from contact search in the results. | | `--consensus` | int | none | Consensus threshold for discovered contacts. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--param` | str | none | Extra key=value query parameter forwarded to the SDK (repeatable). | | `--name` | str | none | Filter by contact name (partial match supported). | | `--summary` | str | none | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. | | `--negate-summary` | str | none | Exclude contacts whose profile summary contains every word of any one term, in any order. | | `--skills` | str | none | Filter by skill (repeatable). | | `--person-state` | str | none | Filter by contact state/region (repeatable). | | `--persona-id` | int | none | Filter by persona ID (repeatable). | | `--filter-state` | str | none | Filter by company state/region (repeatable). | | `--negate-filter-state` | str | none | Exclude contacts at companies in this state (repeatable). | | `--email-validated` | bool | none | Deprecated. Treated as has_email: contact search no longer filters on email verification; verify emails instead. | | `--has-phone` | bool | none | Only contacts with (or without) a phone number. | | `--has-mobile` | bool | none | Only contacts with (or without) a mobile phone number. | | `--has-linkedin` | bool | none | Only contacts with (or without) a LinkedIn profile. | | `--min-connections` | int | none | Minimum LinkedIn connections required. | | `--inclusion-query-id` | str | none | Include only contacts from companies in this saved query (repeatable). | | `--exclusion-query-id` | str | none | Exclude contacts from companies in this saved query (repeatable). | | `--max-companies` | int | none | Maximum number of enriched companies to return; cannot be combined with --max-records. | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | ```bash discolike contacts discover --domain stripe.com --seniority vp \ --results-by-company 5 --include-search-contacts --consensus 2 ``` REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.discover()`. ### contacts generate Generate contacts for target domains from an ICP description (async job). The job is reported with `task_family` `discogen`, so poll it with `--family discogen`. ``` discolike contacts generate [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-text` | str | required | ICP description used to generate contacts. | | `--domain` | str | none | Target company domain (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. One of `--domain` or `--domains-file` is required. | | `--context-mode` | str | none | Context mode for generation. | | `--integration-id` | str | none | Integration ID to use for generation. | | `--search-provider-id` | str | none | Search provider ID to use for generation. | | `--search-context-size` | str | none | Search context size for the search provider. | | `--max-contacts-per-domain` | int | none | Maximum contacts generated per domain. | | `--max-company-records` | int | none | Maximum company records to process. | | `--find-emails` | flag | off | Run the email finder over named, email-less rows before the job completes; fills `email` and `email_status`. Found addresses bill under the finder's rules. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--full-domain` | str | none | Domain to send as full_domains to the generation job (repeatable). | | `--partial-domain` | str | none | Domain to send as partial_domains to the generation job (repeatable). | ```bash discolike contacts generate --icp-text "Heads of RevOps" \ --domain acme.com --domain beta.com --max-contacts-per-domain 5 --find-emails --wait ``` Without `--find-emails`, `email` is null unless a source showed it verbatim; chain `discolike email find-batch` on the returned names to look addresses up afterwards. REST endpoint: [Contacts API](https://docs.discolike.com/api/endpoints/contacts/). SDK: `client.contacts.generate()`. ## count Count companies matching the given filters. Counting is the cheap way to size a query before you run `discover`. ``` discolike count [OPTIONS] ``` The filter set is identical to `discover` minus `--icp-prompt`, `--domain`, `--exclude-domain`, `--exclusion-query-id`, `--max-records` and `--offset`. | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--phrase-match` | str | none | Phrase the company website must contain (repeatable). | | `--negate-phrase-match` | str | none | Negate the `--phrase-match` filter (repeatable). | | `--category` | str | none | Industry category filter (repeatable). | | `--negate-category` | str | none | Negate the `--category` filter (repeatable). | | `--country` | str | none | ISO country code filter (repeatable). | | `--negate-country` | str | none | Negate the `--country` filter (repeatable). | | `--state` | str | none | State or region filter (repeatable). | | `--negate-state` | str | none | Negate the `--state` filter (repeatable). | | `--sub-industry` | str | none | Sub-industry filter, bare or `PARENT/SUB` (repeatable). | | `--negate-sub-industry` | str | none | Negate the `--sub-industry` filter (repeatable). | | `--geo` | str | none | Circular area as `lat,lon` or `lat,lon,radius`, e.g. `30.27,-97.74,30mi` (repeatable). Radius defaults to 50km. | | `--bbox` | str | none | Bounding box as `min_lat,min_lon,max_lat,max_lon` (repeatable). Longitudes may wrap the antimeridian. | | `--lat` | float | none | Latitude of the search centre; requires `--lon`. | | `--lon` | float | none | Longitude of the search centre; requires `--lat`. | | `--radius` | str | none | Radius around `--lat`/`--lon`, e.g. `50km` or `30mi`. Default 50km. | | `--employee-range` | str | none | Employee count range filter. | | `--revenue-range` | str | none | Revenue range filter. | | `--business-model` | str | none | Business model filter (repeatable). | | `--negate-business-model` | str | none | Negate the `--business-model` filter (repeatable). | | `--tech-stack` | str | none | Technology stack filter (repeatable). | | `--negate-tech-stack` | str | none | Negate the `--tech-stack` filter (repeatable). | | `--min-digital-footprint` | int | none | Minimum digital footprint score. | | `--max-digital-footprint` | int | none | Maximum digital footprint score. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--param` | str | none | Extra API parameter as `KEY=VALUE` (comma-separates into a list); see the endpoint reference (repeatable). | | `--subdomain` | str | none | Limit results to this subdomain, up to 20, each at least 3 characters (repeatable). | | `--negate-subdomain` | str | none | Exclude this subdomain, up to 20, each at least 3 characters (repeatable). | | `--language` | str | none | Filter by site language as an ISO 639-1 code, e.g. en, de, pb, zt (repeatable). | | `--negate-language` | str | none | Exclude this site language (repeatable); same values as --language. | | `--social` | str | none | Filter by social platform presence (repeatable): facebook, instagram, linkedin, pinterest, threads, tiktok, twitter, x, yelp, youtube, googleplay, applestore, amazon, vk, bluesky, xing. | | `--negate-social` | str | none | Exclude companies with this social profile (repeatable); same values as --social. | | `--start-date` | str | none | Minimum company start date (YYYY-MM-DD) or range (YYYY-MM-DD,YYYY-MM-DD). | | `--redirect` | bool | none | Include or exclude domains that redirect to another domain. | | `--exclude-leadgen` | bool | none | Exclude suspected lead generation sites (off by default for count). | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | ```bash discolike count --category SOFTWARE --country US ``` ```json { "count": 68900 } ``` ```bash discolike count --country DE --employee-range 50-200 ``` ```json { "count": 1510010 } ``` A malformed `--param` fails before any request is made: ```bash discolike count --param bogus ``` ``` Usage: discolike count [OPTIONS] Try 'discolike count --help' for help. ╭─ Error ──────────────────────────────────────────────────────────────────────╮ │ Invalid value: --param must be in KEY=VALUE form, got 'bogus' │ ╰──────────────────────────────────────────────────────────────────────────────╯ ``` REST endpoint: [Count API](https://docs.discolike.com/api/endpoints/count/). SDK equivalent: [`client.count()`](https://docs.discolike.com/sdk/reference/#count). ## discogen Run DiscoGen research jobs and check status of or cancel any async task (see `--family`). SDK equivalents live on [`client.discogen`](https://docs.discolike.com/sdk/reference/#discogen); job handling is covered in [Job](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). ### discogen run Run a DiscoGen research query across company domains (async job). ``` discolike discogen run [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--query` | str | required | Research query to run. | | `--domain` | str | none | Company domain to research (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. One of `--domain` or `--domains-file` is required. | | `--integration-id` | str | none | Integration ID to use for the run. | | `--web-search` / `--no-web-search` | bool | unset | Toggle web search during research. | | `--context-mode` | str | none | Context mode; see the endpoint reference. | | `--include-x-search` / `--no-include-x-search` | bool | unset | Toggle including X search in the research. | | `--typed-columns` / `--no-typed-columns` | bool | unset | Let the detector answer yes/no, fixed-set and scale columns with a TypeSafe judgment model. | | `--include-confidence` / `--no-include-confidence` | bool | unset | Add a confidence column beside each typed column. | | `--search-provider-id` | str | none | Search provider ID to use for web search. | | `--search-context-size` | str | none | Search context size; see the endpoint reference. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike discogen run --query "Recent funding rounds" \ --domain stripe.com --domain adyen.com --web-search --wait --timeout 600 ``` REST endpoint: [DiscoGen API](https://docs.discolike.com/api/endpoints/discogen/). SDK: `client.discogen.process()`. ### discogen run-personas Run a DiscoGen research query across personas (async job). Identical to `run` except that `--persona-id` replaces `--domain`. ``` discolike discogen run-personas [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--query` | str | required | Research query to run. | | `--persona-id` | int | required | Persona ID to research (repeatable). | | `--integration-id` | str | none | Integration ID to use for the run. | | `--web-search` / `--no-web-search` | bool | unset | Toggle web search during research. | | `--context-mode` | str | none | Context mode; see the endpoint reference. | | `--include-x-search` / `--no-include-x-search` | bool | unset | Toggle including X search in the research. | | `--typed-columns` / `--no-typed-columns` | bool | unset | Let the detector answer yes/no, fixed-set and scale columns with a TypeSafe judgment model. | | `--include-confidence` / `--no-include-confidence` | bool | unset | Add a confidence column beside each typed column. | | `--search-provider-id` | str | none | Search provider ID to use for web search. | | `--search-context-size` | str | none | Search context size; see the endpoint reference. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike discogen run-personas --query "Career history" --persona-id 1 --persona-id 2 ``` REST endpoint: [DiscoGen API](https://docs.discolike.com/api/endpoints/discogen/). SDK: `client.discogen.process_personas()`. ### discogen models List the models available for DiscoGen research. ``` discolike discogen models [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike discogen models ``` ```json { "models": { "openai": [ { "name": "o4-mini", "supports_web_search": true }, { "name": "gpt-5-pro", "supports_web_search": true }, ... ], ... } } ``` REST endpoint: [DiscoGen API](https://docs.discolike.com/api/endpoints/discogen/). SDK: `client.discogen.models()`. ### discogen status Show the status and progress of an async task. This is the polling command for every async job in the CLI, not just DiscoGen. ``` discolike discogen status [OPTIONS] TASK_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `TASK_ID` (argument) | str | required | Task ID returned when the job was started. | | `--family` | `discogen` \| `bulkmatch` \| `contactmatch` \| `segment` | `discogen` | Task family the task_id belongs to (bulk match jobs are 'bulkmatch', segment jobs 'segment', contact bulk-match 'contactmatch'). | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike discogen status dg-4 ``` ```bash discolike discogen status seg-1 --family segment ``` Any other `--family` value is a usage error and exits 2. SDK: [`Job.status()`](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). ### discogen cancel Cancel an async task. Output is always JSON: `{"cancelled": ""}`. ``` discolike discogen cancel [OPTIONS] TASK_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `TASK_ID` (argument) | str | required | Task ID returned when the job was started. | | `--family` | `discogen` \| `bulkmatch` \| `contactmatch` \| `segment` | `discogen` | Task family the task_id belongs to (bulk match jobs are 'bulkmatch', segment jobs 'segment', contact bulk-match 'contactmatch'). | ```bash discolike discogen cancel bm-1 --family bulkmatch ``` SDK: [`Job.cancel()`](https://docs.discolike.com/sdk/reference/#job-and-asyncjob). ## discover Discover companies matching your ICP and filters. ``` discolike discover [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp-prompt` | str | none | Natural-language ideal customer profile description. | | `--domain` | str | none | Seed domain for lookalike matching (repeatable). | | `--phrase-match` | str | none | Phrase the company website must contain (repeatable). | | `--negate-phrase-match` | str | none | Negate the `--phrase-match` filter (repeatable). | | `--category` | str | none | Industry category filter (repeatable). | | `--negate-category` | str | none | Negate the `--category` filter (repeatable). | | `--country` | str | none | ISO country code filter (repeatable). | | `--negate-country` | str | none | Negate the `--country` filter (repeatable). | | `--state` | str | none | State or region filter (repeatable). | | `--negate-state` | str | none | Negate the `--state` filter (repeatable). | | `--sub-industry` | str | none | Sub-industry filter, bare or `PARENT/SUB` (repeatable). | | `--negate-sub-industry` | str | none | Negate the `--sub-industry` filter (repeatable). | | `--geo` | str | none | Circular area as `lat,lon` or `lat,lon,radius`, e.g. `30.27,-97.74,30mi` (repeatable). Radius defaults to 50km. | | `--bbox` | str | none | Bounding box as `min_lat,min_lon,max_lat,max_lon` (repeatable). Longitudes may wrap the antimeridian. | | `--lat` | float | none | Latitude of the search centre; requires `--lon`. | | `--lon` | float | none | Longitude of the search centre; requires `--lat`. | | `--radius` | str | none | Radius around `--lat`/`--lon`, e.g. `50km` or `30mi`. Default 50km. | | `--employee-range` | str | none | Employee count range filter. | | `--revenue-range` | str | none | Revenue range filter. | | `--business-model` | str | none | Business model filter (repeatable). | | `--negate-business-model` | str | none | Negate the `--business-model` filter (repeatable). | | `--tech-stack` | str | none | Technology stack filter (repeatable). | | `--negate-tech-stack` | str | none | Negate the `--tech-stack` filter (repeatable). | | `--min-digital-footprint` | int | none | Minimum digital footprint score. | | `--max-digital-footprint` | int | none | Maximum digital footprint score. | | `--exclude-domain` | str | none | Domain to exclude from results (repeatable). | | `--exclude-domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--exclude-domain` (100 domains max, checked before the request). | | `--exclusion-query-id` | str | none | Saved query ID whose results are excluded (repeatable). | | `--max-records` | int | none | Maximum number of companies to return. | | `--offset` | int | none | Number of records to skip for pagination. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--param` | str | none | Extra API parameter as `KEY=VALUE` (comma-separates into a list); see the endpoint reference (repeatable). | | `--subdomain` | str | none | Limit results to this subdomain, up to 20, each at least 3 characters (repeatable). | | `--negate-subdomain` | str | none | Exclude this subdomain, up to 20, each at least 3 characters (repeatable). | | `--language` | str | none | Filter by site language as an ISO 639-1 code, e.g. en, de, pb, zt (repeatable). | | `--negate-language` | str | none | Exclude this site language (repeatable); same values as --language. | | `--social` | str | none | Filter by social platform presence (repeatable): facebook, instagram, linkedin, pinterest, threads, tiktok, twitter, x, yelp, youtube, googleplay, applestore, amazon, vk, bluesky, xing. | | `--negate-social` | str | none | Exclude companies with this social profile (repeatable); same values as --social. | | `--start-date` | str | none | Minimum company start date (YYYY-MM-DD) or range (YYYY-MM-DD,YYYY-MM-DD). | | `--min-similarity` | int | none | Minimum similarity score to include (0-99). | | `--relevance` | str | none | Where a lookalike list stops: MAX_RELEVANCE (shortest, tightest), BALANCED (the app's default), MAX_COVERAGE (longest, more borderline companies). Omit for no automatic stop. | | `--variance` | str | none | Deprecated. LOW, MID_LOW, MEDIUM act as MAX_RELEVANCE; MID_HIGH, HIGH as BALANCED; UNRESTRICTED as no stop; `--relevance` wins when both are sent. Use `--relevance`. | | `--consensus` | int | none | Deprecated, no-op. Discover applies consensus itself to single-domain lookalike searches. | | `--redirect` | bool | none | Include or exclude domains that redirect to another domain. | | `--exclude-leadgen` | bool | none | Exclude suspected lead generation sites (on by default). | | `--retrieval` | bool | none | Enable page data retrieval using the Extract API. | | `--enhanced` | bool | none | Deprecated, no-op. Discover no longer adds AI-suggested domains. | | `--include-search-domains` | bool | none | Include the input domains in results (excluded by default). | | `--auto-icp-text` | bool | none | Auto-generate ICP text from the provided domain(s). | | `--auto-phrase-match` | bool | none | Auto-generate phrase matches from ICP text. | | `--inclusion-query-id` | str | none | Saved query ID whose domains are included (repeatable); requires the STARTER plan. | | `--params-file` | path | none | JSON object of API parameter names (an app form copied over); `--param` and named flags override its values. | Repeatable flags accumulate into a list, so `--country US --country CA` sends both values. Every `--geo` circle, every `--bbox` box and the `--lat`/`--lon`/`--radius` centre are OR'd together, up to 10 shapes per query: `discolike discover --geo 30.27,-97.74,30mi --geo 52.52,13.405` searches both areas. `--param` is the escape hatch for filters that have no dedicated flag. It splits on the first `=`, and a value containing a comma becomes a list - `--param social=linkedin,youtube` sends `social=linkedin&social=youtube`. A value with no `=` is a usage error (exit 2); a value the API schema rejects exits 2 before the request is sent. Named flags win over `--param` for the same key: `--country DE --param country=US` sends `country=DE`. `--params-file form.json` loads a whole request from a JSON object keyed by API parameter names, the shape you get by copying an app form over; it sits under `--param`, which sits under the named flags. The file is validated against the request schema before anything is sent, so a bad enum value exits 2 with no request made. ```bash discolike discover --params-file form.json --param min_similarity=50 --relevance BALANCED --max-records 100 ``` ```bash discolike discover --icp-prompt "B2B payments infrastructure" \ --country US --employee-range 50-200 --max-records 3 ``` ```bash discolike discover --domain stripe.com --domain adyen.com \ --negate-country RU --exclude-domain stripe.com --max-records 3 ``` REST endpoint: [Discover API](https://docs.discolike.com/api/endpoints/discover/). SDK equivalent: [`client.discover()`](https://docs.discolike.com/sdk/reference/#discover). ## email Find work email addresses: submit single or batch find jobs, poll them, and fetch results. Only proven addresses bill; catch-all and pattern guesses are free. Every submission is asynchronous. `find` and `find-batch` return an ID immediately; add `--wait` to block until the result is ready, or poll later with `email job` and `email results`. SDK equivalents live on [`client.email`](https://docs.discolike.com/sdk/reference/#email). Email jobs use their own IDs and their own polling commands, separate from `discogen status`. ### email find Submit a single email find job (async); only a proven address bills. ``` discolike email find [OPTIONS] FIRST_NAME LAST_NAME DOMAIN ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `FIRST_NAME` (argument) | str | required | First name of the person. | | `LAST_NAME` (argument) | str | required | Last name of the person. | | `DOMAIN` (argument) | str | required | Company domain to search, e.g. acme.com. | | `--known-pattern` | str | none | Known email local-part pattern for this domain, e.g. first.last. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | Without `--wait` the command prints the job ID and the command that polls it. The payload is a single object, so it always prints as JSON regardless of `--format`. ```bash discolike email find Patrick Collison stripe.com ``` ```json { "job_id": "98e10684-a270-4167-85a6-dc638bcf3791", "hint": "poll with: discolike email job 98e10684-a270-4167-85a6-dc638bcf3791" } ``` With `--wait` the command blocks and prints the enumeration result instead, writing `status: ` lines to stderr on each poll. ```bash discolike email find Patrick Collison stripe.com --wait --timeout 120 ``` Pass `--known-pattern` when you already know how a domain builds its addresses; it is omitted from the request entirely when you leave it off. ```bash discolike email find Ada Lovelace acme.com --known-pattern first.last --wait ``` REST endpoint: [Email Find API](https://docs.discolike.com/api/endpoints/email-find/). SDK: `client.email.find()`. ### email find-batch Submit an email find batch from a CSV file and/or inline contacts (async). ``` discolike email find-batch [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--contacts-file` | path | none | Path to a CSV file with first_name,last_name,domain columns (max 500 contacts per batch). | | `--contact` | str | none | Inline contact as "first_name,last_name,domain" (repeatable). | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | Provide `--contacts-file`, one or more `--contact`, or both; the two sources are concatenated, file rows first. Supplying neither exits 2, and so does a total above 500 contacts or a CSV missing any of the three required columns. ```bash discolike email find-batch --contacts-file people.csv \ --contact "Ada,Lovelace,acme.com" --wait --timeout 600 ``` Without `--wait` you get the batch ID and the command that fetches it: ```json { "batch_id": "7f6f0a4e-52f7-4f3e-9c2d-0b8d3f1c9a11", "hint": "fetch with: discolike email results 7f6f0a4e-52f7-4f3e-9c2d-0b8d3f1c9a11" } ``` With `--wait` the progress lines on stderr read `progress: / completed, failed`. REST endpoint: [Email Find API](https://docs.discolike.com/api/endpoints/email-find/). SDK: `client.email.find_batch()`. ### email results Fetch results for an email find or verify batch. ``` discolike email results [OPTIONS] BATCH_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `BATCH_ID` (argument) | str | required | Batch ID returned by `discolike email find-batch`. | | `--kind` | `find` \| `verify` | `find` | Batch kind: find or verify (verify batches are created by the DiscoLike app). | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | Without `--wait` the command fetches one snapshot and prints it even if the batch is still running, so `completed` and `total` can disagree. Any other `--kind` value is a usage error and exits 2. `--kind` selects how each result is decoded: `find` reads them as enumeration results, `verify` as validation results. Pass the `--kind` that matches the batch. ```bash discolike email results 7f6f0a4e-52f7-4f3e-9c2d-0b8d3f1c9a11 --wait ``` ```bash discolike email results --kind verify ``` REST endpoint: [Email Find API](https://docs.discolike.com/api/endpoints/email-find/). SDK: `client.email.batch()`. ### email job Poll a single email find job: print its current status, or block with `--wait`. The command is find-only and has no `--kind` flag; rehydrating a verify job needs the SDK's `client.email.job(job_id, kind="verify")`. ``` discolike email job [OPTIONS] JOB_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `JOB_ID` (argument) | str | required | Job ID returned by `discolike email find`. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | Without `--wait` you get the job wrapper: lifecycle `status` on the outside, the enumeration result nested under `result`: ```bash discolike email job 98e10684-a270-4167-85a6-dc638bcf3791 ``` ```json { "job_id": "98e10684-a270-4167-85a6-dc638bcf3791", "status": "completed", "result": { "first_name": "patrick", "last_name": "collison", "domain": "stripe.com", "status": "catch_all_pattern", "result": { "email": "patrick.collison@stripe.com", "pattern": "first.last", "tier": 1, "smtp_code": 0, "valid": false }, "is_catch_all": true, "mx_host": "aspmx3.googlemail.com", "provider": "Everything Else", "attempts": 0, "duration_ms": 1473, "error": null }, "error": null } ``` With `--wait` the wrapper is stripped and only the enumeration result is printed: ```bash discolike email job 98e10684-a270-4167-85a6-dc638bcf3791 --wait --timeout 120 ``` ```json { "first_name": "patrick", "last_name": "collison", "domain": "stripe.com", "status": "catch_all_pattern", "result": { "email": "patrick.collison@stripe.com", "pattern": "first.last", "tier": 1, "smtp_code": 0, "valid": false }, "is_catch_all": true, "mx_host": "aspmx3.googlemail.com", "provider": "Everything Else", "attempts": 0, "duration_ms": 1473, "error": null } ``` Polling is free; only the original `find` can bill, and only when it proves an address. The two `status` fields mean different things: the outer one is the job lifecycle, the inner one is the enumeration outcome. See [Email Find](https://docs.discolike.com/api/endpoints/email-find/) for every value the inner `status` can take. REST endpoint: [Email Find API](https://docs.discolike.com/api/endpoints/email-find/). SDK: `client.email.job()`. ## extract Extract page content from a URL. ``` discolike extract [OPTIONS] URL ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `URL` (argument) | str | required | Page URL to extract content from. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--domain` | str | none | Bare domain to extract instead of URL (alias for https://DOMAIN, served from cache). | ```bash discolike extract https://stripe.com/about ``` ```json { "text": "Stripe | Financial Infrastructure to Grow Your Revenue Products Solutions Developers Resources Pricing Sign in Start now Contact sales ...", "language": "en" } ``` REST endpoint: [Extract API](https://docs.discolike.com/api/endpoints/extract/). SDK equivalent: [`client.companies.extract()`](https://docs.discolike.com/sdk/reference/#companiesextract). ## llm-providers Manage BYOK LLM provider integrations (OpenAI, Anthropic, custom endpoints). SDK equivalents live on [`client.llm_providers`](https://docs.discolike.com/sdk/reference/#llm_providers); the REST surface is the [LLM Providers API](https://docs.discolike.com/api/endpoints/llm-providers/). ### llm-providers list List LLM provider integrations for the organization. ``` discolike llm-providers list [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike llm-providers list ``` ```json { "providers": [ { "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": null, "supports_web_search": true, "input_cost_per_token": 2e-06, "output_cost_per_token": 6e-06, "is_default": true, "model_deprecated": false }, ... ] } ``` ### llm-providers create Create an LLM provider integration. No `--format` flag. ``` discolike llm-providers create [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--name` | str | required | User-friendly name for the integration. | | `--provider` | str | required | LLM provider name: openai, anthropic, or custom. Free-form text - not validated against a list. | | `--api-key` | str | required | API key for the provider. | | `--model-name` | str | required | Default model name for this integration. | | `--base-url` | str | none | Endpoint URL (required for the custom provider). | | `--region` | str | none | Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) for watsonx. `discogen models` lists them under `provider_regions`. Appended to `--model-name` as `#region=...`. | | `--account` | str | none | Snowflake account identifier, e.g. `orgname-accountname` (snowflake only). Appended to `--model-name` as `#account=...`. | | `--project` | str | none | watsonx project ID (watsonx only). Appended to `--model-name` as `#project=...`. | ```bash discolike llm-providers create --name OpenAI --provider openai \ --api-key sk-... --model-name gpt-4o ``` ### llm-providers get Get a single LLM provider integration by ID. ``` discolike llm-providers get [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the LLM provider integration to fetch. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike llm-providers get 1f5e8466-fda4-4d4e-88eb-4b0886c37003 ``` ```json { "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": null, "supports_web_search": true, "input_cost_per_token": 2e-06, "output_cost_per_token": 6e-06, "is_default": true, "model_deprecated": false } ``` ### llm-providers update Update an LLM provider integration. `--name`, `--provider` and `--model-name` are required on every update. Omitting `--api-key` keeps the stored key. No `--format` flag. ``` discolike llm-providers update [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the LLM provider integration to update. | | `--name` | str | required | User-friendly name for the integration. | | `--provider` | str | required | LLM provider name: openai, anthropic, or custom. | | `--model-name` | str | required | Default model name for this integration. | | `--api-key` | str | none | New API key (omit to keep the stored key). | | `--base-url` | str | none | Endpoint URL (required for the custom provider). | | `--region` | str | none | Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) for watsonx. `discogen models` lists them under `provider_regions`. Appended to `--model-name` as `#region=...`. | | `--account` | str | none | Snowflake account identifier, e.g. `orgname-accountname` (snowflake only). Appended to `--model-name` as `#account=...`. | | `--project` | str | none | watsonx project ID (watsonx only). Appended to `--model-name` as `#project=...`. | ```bash discolike llm-providers update 1f5e8466-fda4-4d4e-88eb-4b0886c37003 \ --name Anthropic --provider anthropic --model-name claude-sonnet-4-5 ``` ### llm-providers delete Delete an LLM provider integration (admin only). Prints `{"deleted": ""}`. ``` discolike llm-providers delete [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the LLM provider integration to delete. | ```bash discolike llm-providers delete 1f5e8466-fda4-4d4e-88eb-4b0886c37003 ``` ### llm-providers set-default Set an LLM provider integration as the organization default (admin only). The default is used whenever a command omits `--integration-id`. ``` discolike llm-providers set-default [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the integration to make the organization default. | ```bash discolike llm-providers set-default 1f5e8466-fda4-4d4e-88eb-4b0886c37003 ``` ### llm-providers test-connection Test a provider configuration before saving it. Nothing is persisted. No `--format` flag. ``` discolike llm-providers test-connection [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--name` | str | `cli-test` | Name to label the test configuration. | | `--provider` | str | required | LLM provider name: openai, anthropic, or custom. | | `--api-key` | str | required | API key for the provider. | | `--model-name` | str | required | Model name to test against. | | `--base-url` | str | none | Endpoint URL (required for the custom provider). | | `--region` | str | none | Provider region: an AWS region for bedrock and bedrock_mantle, a watsonx region (us-south, eu-de, ...) for watsonx. `discogen models` lists them under `provider_regions`. Appended to `--model-name` as `#region=...`. | | `--account` | str | none | Snowflake account identifier, e.g. `orgname-accountname` (snowflake only). Appended to `--model-name` as `#account=...`. | | `--project` | str | none | watsonx project ID (watsonx only). Appended to `--model-name` as `#project=...`. | ```bash discolike llm-providers test-connection --provider openai \ --api-key sk-... --model-name gpt-4o ``` ## match Match a company name to a domain, or bulk-match a CSV of names. ``` discolike match [OPTIONS] [NAME] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `NAME` (argument) | str | none | Company name to match to a domain. Optional, but exactly one of `NAME` or `--file` is required. | | `--phone` | str | none | Phone number to disambiguate the match. | | `--city` | str | none | City to disambiguate the match. | | `--state` | str | none | State or region to disambiguate the match. | | `--country` | str | none | Country to disambiguate the match. | | `--zip-code` | str | none | ZIP or postal code to disambiguate the match. | | `--strict` / `--no-strict` | bool | unset | Toggle strict matching. Omitted from the request entirely when neither form is passed. | | `--local-mode` / `--no-local-mode` | bool | unset | Toggle local matching mode. Omitted from the request entirely when neither form is passed. | | `--file` | path | none | CSV of company names to bulk-match as an async job (instead of `NAME`). | | `--name-column` | str | `name` | Column in `--file` that holds company names. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--min-match-confidence` | int | none | Minimum match_confidence (inclusive, 50-100) a match must have to be returned. | | `--phone-column` | str | none | Column in --file that holds phone numbers. | | `--city-column` | str | none | Column in --file that holds cities. | | `--state-column` | str | none | Column in --file that holds states. | | `--country-column` | str | none | Column in --file that holds country codes. | | `--zip-code-column` | str | none | Column in --file that holds zip codes. | Passing both `NAME` and `--file`, or neither, is a usage error (exit 2). With `NAME` the call is synchronous; with `--file` it starts a `bulkmatch` job. ```bash discolike match "Stripe Inc" ``` ```json { "query": { "name": "Stripe Inc", "country": null, "state": null, "city": null, "zip": null, "phones": null }, "matches": [ { "domain": "stripe.com", "name": "Stripe", "status": { "status": "active", "confidence": 0.89 }, "score": 701, "start_date": "2011-01-04", "end_date": null, "address": { "street": "354 Oyster Point Blvd", "city": "South San Francisco", "state": "California", "zip": "94080", "country": "US" }, "phones": [ "+16504279276", "+18889262289" ], ... } ] } ``` Disambiguate with location fields when the name is ambiguous: ```bash discolike match "Acme" --city Boston --state MA --country US --strict ``` Bulk-match a CSV and wait for the job to finish: ```bash discolike match --file names.csv --name-column company --wait --timeout 600 ``` REST endpoints: [Match API](https://docs.discolike.com/api/endpoints/match/) for a single name, [Bulk Match API](https://docs.discolike.com/api/endpoints/bulk-match/) for `--file`. SDK equivalents: [`client.match.company()` and `client.match.bulk()`](https://docs.discolike.com/sdk/reference/#match). ## queries Manage saved queries and exclusion lists for reusable targeting. SDK equivalents live on [`client.queries`](https://docs.discolike.com/sdk/reference/#queries); the REST surface is the [Queries API](https://docs.discolike.com/api/endpoints/queries/). ### queries list List saved queries. ``` discolike queries list [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--max-records` | int | none | Maximum number of saved queries to return. | | `--offset` | int | none | Number of records to skip for pagination. | | `--action` | str | none | Filter by query action, e.g. discover. Free-form text - not validated against a list. | | `--tag` | str | none | Filter by tag (repeatable). | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike queries list --max-records 1 --action discover ``` ```json { "results": [ { "query_id": "e15abe19-a8dc-41ca-8c79-38047ede6450", "query_name": "USA law firms in United States", "action": "discover", "user_name": "George Rekouts", "mtime": "2026-08-19T20:25:35.912744", "domains": [ "legalpages.co.uk", "myfamilyattorneys.com", ... ], ... } ] } ``` ### queries create-exclusion-list Create a named exclusion list of domains and/or persona IDs. No `--format` flag. ``` discolike queries create-exclusion-list [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--name` | str | required | Name for the new exclusion list. | | `--domain` | str | none | Domain to exclude (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line; merged with `--domain`. | | `--persona-id` | int | none | Persona ID to exclude (repeatable). | | `--tag` | str | none | Tag to attach to the list (repeatable). | ```bash discolike queries create-exclusion-list --name "Current customers" \ --domain acme.com --domain beta.com --tag crm ``` A page of 10,000 discover results goes in from its CSV: ```bash discolike queries create-exclusion-list --name "tam-round-1" --domains-file round1.csv --tag tam ``` Feed the resulting query ID back into discovery with `discolike discover --exclusion-query-id `. ### queries save-results Save result rows from a file as a reusable saved query. No `--format` flag. ``` discolike queries save-results [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--input` | path | required | Path to a .json (list of row objects) or .csv (header row) file. | | `--name` | str | required | Name for the saved query. | | `--action` | `discover` \| `segment` \| `contacts` \| `append` \| `match` | required | Underlying action: discover, segment, contacts, append, or match. | | `--domain-column` | str | `domain` | Column holding domains. | | `--tag` | str | none | Tag to attach (repeatable). | The file suffix decides the parser: `.csv` (case-insensitive) is read with a CSV reader and every value stays a string; anything else is parsed as JSON. A missing file or invalid JSON is a usage error (exit 2), as is an `--action` outside the five choices. ```bash discolike queries save-results --input rows.json --name "Q3 targets" --action discover ``` ```bash discolike queries save-results --input rows.csv --name "Q3 targets" \ --action discover --domain-column website --tag q3 ``` ### queries update Rename a saved query and/or update its tags. No `--format` flag. ``` discolike queries update [OPTIONS] QUERY_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `QUERY_ID` (argument) | str | required | ID of the saved query to update. | | `--name` | str | none | New name for the saved query. | | `--tag` | str | none | Tag to set on the query (repeatable). | ```bash discolike queries update e15abe19-a8dc-41ca-8c79-38047ede6450 --name "Renamed" --tag hot ``` ### queries delete Delete a saved query. No options beyond the argument; prints `{"deleted": ""}`. ``` discolike queries delete [OPTIONS] QUERY_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `QUERY_ID` (argument) | str | required | ID of the saved query to delete. | ```bash discolike queries delete e15abe19-a8dc-41ca-8c79-38047ede6450 ``` ## search-providers Manage BYOK web search provider integrations (Tavily, Serper, etc.). SDK equivalents live on [`client.search_providers`](https://docs.discolike.com/sdk/reference/#search_providers); the REST surface is the [Search Providers API](https://docs.discolike.com/api/endpoints/search-providers/). ### search-providers list List search provider integrations for the organization. ``` discolike search-providers list [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike search-providers list ``` ```json { "providers": [ { "integration_id": "fd467f7f-11c1-4475-923a-f2c3341e41bb", "integration_name": "Serper", "provider": "serper", "search_model": "serper/search", "encrypted_api_key": null, "api_key": "*****", "base_url": null, "is_default": true, "cost_per_query": 0.001 }, ... ] } ``` ### search-providers create Create a search provider integration (connectivity is validated first). No `--format` flag. ``` discolike search-providers create [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--name` | str | required | User-friendly name for the integration. | | `--provider` | str | required | Search provider key, e.g. tavily or serper. | | `--search-model` | str | required | Search model key, e.g. tavily/search. | | `--api-key` | str | none | Provider API key (omit for free providers). | | `--base-url` | str | none | Custom endpoint URL for a LiteLLM proxy. | ```bash discolike search-providers create --name Tavily --provider tavily \ --search-model tavily/search --api-key tvly-... ``` ### search-providers update Update a search provider integration. `--name`, `--provider` and `--search-model` are required on every update - this is a full replace, not a patch. No `--format` flag. ``` discolike search-providers update [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the search provider integration to update. | | `--name` | str | required | User-friendly name for the integration. | | `--provider` | str | required | Search provider key, e.g. tavily or serper. | | `--search-model` | str | required | Search model key, e.g. tavily/search. | | `--api-key` | str | none | New provider API key (omit to keep the stored key). | | `--base-url` | str | none | Custom endpoint URL for a LiteLLM proxy. | ```bash discolike search-providers update fd467f7f-11c1-4475-923a-f2c3341e41bb \ --name Serper --provider serper --search-model serper/search ``` ### search-providers delete Delete a search provider integration (admin only). Prints `{"deleted": ""}`. ``` discolike search-providers delete [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the search provider integration to delete. | ```bash discolike search-providers delete fd467f7f-11c1-4475-923a-f2c3341e41bb ``` ### search-providers set-default Set a search provider integration as the organization default (admin only). ``` discolike search-providers set-default [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the integration to make the organization default. | ```bash discolike search-providers set-default fd467f7f-11c1-4475-923a-f2c3341e41bb ``` ### search-providers clear-default Remove the default flag from a search provider integration (admin only). ``` discolike search-providers clear-default [OPTIONS] INTEGRATION_ID ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `INTEGRATION_ID` (argument) | str | required | ID of the integration currently set as default. | ```bash discolike search-providers clear-default fd467f7f-11c1-4475-923a-f2c3341e41bb ``` ### search-providers models List available search models grouped by provider, with cost per query. Use this to find the exact `--search-model` value for `create` and `update`. ``` discolike search-providers models [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike search-providers models ``` ```json { "models": { "apiserpent": [ { "name": "apiserpent/search/google", "cost_per_query": 0.0006 }, { "name": "apiserpent/search/bing", "cost_per_query": 0.0006 }, ... ], ... } } ``` ## segment Auto-segment a list of domains (async job). The task family is `segment`. ``` discolike segment [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--domain` | str | none | Domain to segment (repeatable). | | `--file` | path | none | CSV of domains to segment (instead of `--domain`). | | `--domain-column` | str | none | Column in `--file` that holds domains. | | `--max-segments` | int | none | Maximum number of segments to produce. 2-20 with `--domain`, 2-100 with `--file`. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | | `--query-id` | str | none | Saved query ID whose domains are included alongside the file/--domain ones (repeatable). | Pass exactly one of `--domain` or `--file`; both or neither is a usage error (exit 2). ```bash discolike segment --domain acme.com --domain beta.com --max-segments 3 ``` Without `--wait` the command prints a job handle - `task_id`, `task_family` (`segment`), and a `hint` string with the exact `discolike discogen status` command to poll it - and exits immediately. ```bash discolike segment --file domains.csv --domain-column domain --wait --timeout 600 ``` REST endpoint: [Segment API](https://docs.discolike.com/api/endpoints/segment/). SDK equivalent: [`client.segment()`](https://docs.discolike.com/sdk/reference/#segment) for `--domain`, [`client.segment_file()`](https://docs.discolike.com/sdk/reference/#segment_file) for `--file`. ## signup Create a DiscoLike account for a person. No login needed; they confirm by email. This is the one command that works before `discolike auth login`. ``` discolike signup [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--email` | str | required | The person's work email. Becomes the login. | | `--first-name` | str | required | 1-40 characters. Must contain a letter and no angle brackets or control characters. | | `--last-name` | str | required | Same rules as `--first-name`. | | `--agent` | str | none | Agent or framework name to record with the signup. Defaults to `discolike-cli/`. | | `--yes`, `-y` | flag | off | Skip the confirmation when signing up a different email than last time. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | ```bash discolike signup --email jane@acme.com --first-name Jane --last-name Doe ``` ```json { "status": "created", "email": "jane@acme.com", "org_domain": "acme.com", "org_status": "created", "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." } ``` Relay `next_step` to the person: they confirm by email, log in at [app.discolike.com](https://app.discolike.com), pick a plan, and issue an API key for `discolike auth login`. The command remembers the email it signed up. Running it again with a different email asks for confirmation first, and on a non-TTY exits 1 telling you to re-run with `--yes`, so an agent cannot quietly create a second account. REST endpoint: [Signup API](https://docs.discolike.com/api/endpoints/signup/). SDK equivalent: [`signup()`](https://docs.discolike.com/sdk/reference/#signup). Per-agent instructions: [Agent signup guide](https://docs.discolike.com/guides/agent-signup/). ## validate-icp Validate a list of domains against an ICP definition (async job). ``` discolike validate-icp [OPTIONS] ``` | Flag | Type | Default | Description | | --- | --- | --- | --- | | `--icp` | str | required | ICP definition text to validate the domains against. | | `--domain` | str | none | Domain to validate (repeatable). | | `--domains-file` | path | none | CSV with a `domain` column, or one domain per line (instead of `--domain`). | | `--file` | path | none | Older spelling of `--domains-file`. | | `--context-mode` | str | none | Context mode; see the endpoint reference. | | `--integration-id` | str | none | Integration ID to use for the validation. | | `--web-search` / `--no-web-search` | bool | unset | Toggle web search during validation. | | `--search-provider-id` | str | none | Search provider ID to use for web search. | | `--wait` | flag | off | Block until the job finishes, streaming progress to stderr. | | `--timeout` | float | `900.0` | Max seconds to wait with `--wait`. | | `--format` | str | none | Output format: json or table (table auto-selected on a TTY; falls back to JSON for non-tabular data). | Pass exactly one of `--domain` or `--domains-file` (`--file` is the older spelling of the same flag); both or neither is a usage error (exit 2). The file is a CSV with a `domain` column, or plain text with one domain per line; domains are lower-cased, stripped of `www.`, and de-duplicated. ```bash discolike validate-icp --icp "Mid-market SaaS selling to finance teams" \ --domain acme.com --domain beta.com ``` ```bash discolike validate-icp --icp "Mid-market SaaS" --domains-file domains.txt \ --web-search --wait --timeout 600 ``` REST endpoint: [Validate API](https://docs.discolike.com/api/endpoints/validate/). SDK equivalent: [`client.validate_icp()`](https://docs.discolike.com/sdk/reference/#validate_icp). ## prospecting Plan, approve, and steer managed prospecting. Existing processing/provider charges apply. See the [Prospecting API](https://docs.discolike.com/api/endpoints/prospecting/) and [SDK reference](https://docs.discolike.com/sdk/reference/#prospecting). ### prospecting start ```bash discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1 ``` Returns a run ID in `drafting`. Wait for `proposed`, review the plan, then approve its version. Reuse the submission key on retries. Omitted quantity flags allow brief inference, falling back to 1000 companies and 3 contacts per company. Explicit flags override the text, even when set to those defaults. Work caps do not cap provider-dollar spending; set a provider spending limit on the proposed plan with [prospecting plan](#prospecting-plan) `--max-provider-spend-usd`. | Flag | Default | Description | | --- | --- | --- | | --brief | required | Company requirements and buyer roles | | --idempotency-key | required | Stable submission key, 1–128 characters | | --domain | None | Starting domain, repeatable | | --company-name | None | Company name to resolve, repeatable | | --customers-file | None | Your customers (CSV with a `domain` column, or one per line), up to 1,000: grouped into segments, lookalikes of each are found. Not with --domain or --company-name | | --exclude-domain | None | Suppressed domain, repeatable | | --target-companies | None | Explicit company count, 1–10000; otherwise inferred | | --contacts-per-company | None | Explicit accepted contacts per company, 1–10; otherwise inferred | | --max-candidates | None | Automatic when omitted or 0; maximum 100000 | | --max-actions | None | Automatic when omitted or 0; maximum 10000 | | --validation-integration-id | None | Validation integration | | --contact-integration-id | None | Contact research integration | | --search-provider-id | None | Search provider integration | | --segment | false | Segment qualified companies; --no-segment disables it | | --auto | false | Never pause at checkpoints; see the [Prospecting API's Checkpoints section](https://docs.discolike.com/api/endpoints/prospecting/#checkpoints) | | --deliverable | None | leads (the API default) finds people at the companies; accounts returns the checked companies only | | --goal | None | leads (the API default) keeps adding rounds until the target is met; companies works only --target-companies matching companies | ### prospecting list `discolike prospecting list` lists recent organization runs, newest first. | Flag | Default | Description | | --- | --- | --- | | --limit | 20 | Number of runs, 1–50 | | --before | None | Page past this run ID (the last run ID from a prior page) | ### prospecting plan ```bash discolike prospecting plan RUN_ID --plan-version 1 --target-companies 250 --max-provider-spend-usd 40 ``` Changes a proposed plan's engines, size, deliverable or spending limits before you approve it. Flags you leave out keep their current choice. Engine ids come from the latest plan message's `contact_engine`, `company_check_engine` and `search_provider` options; an engine shown with `selected` null must be picked here before approve. The run comes back re-estimated at a new `plan_version`: approve that one. | Flag | Default | Description | | --- | --- | --- | | --plan-version | required | The plan version you reviewed, at least 1 | | --contact-integration-id | None | An option id from the plan's `contact_engine` | | --validation-integration-id | None | An option id from the plan's `company_check_engine` | | --search-provider-id | None | An option id from the plan's `search_provider`; `none` skips web research | | --target-companies | None | Qualified companies to aim for, 1–10000 | | --contacts-per-company | None | Most contacts to find at each company, 1–10 | | --deliverable | None | leads finds people at the companies; accounts returns the checked companies only | | --goal | None | leads keeps adding rounds until the target is met; companies works only --target-companies matching companies | | --max-spend-usd | None | USD limit on DiscoLike records and per-call fees at your plan's rates; 0 removes the limit | | --max-provider-spend-usd | None | USD limit on your own AI and search provider keys; 0 removes the limit | ### prospecting approve `discolike prospecting approve RUN_ID --plan-version 1` approves the exact version you reviewed. Repeating the approved version is safe; a stale version is rejected. | Flag | Default | Description | | --- | --- | --- | | --plan-version | required | Reviewed plan version, at least 1 | | --seed-segment | None | A customer segment id from the plan card to expand, repeatable; omitted keeps the card's selection | | --segment / --no-segment | None | Group the finished results into segments; omitted keeps the brief's setting | | --auto | false | Never pause at checkpoints for the rest of the run; see the [Prospecting API's Checkpoints section](https://docs.discolike.com/api/endpoints/prospecting/#checkpoints) | ### prospecting message ```bash discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1 ``` Sends steering, a clarification answer, or a question. Returns the stored user message; the agent replies asynchronously. Poll `status --messages-after` with the last returned `next_message_seq` and inspect `reply_pending`. A "segment these" request keeps `reply_pending` true until the segments message is posted, which can take more than an hour. If grouping stops without an outcome, the flag clears on its own after about 90 minutes. | Flag | Default | Description | | --- | --- | --- | | --text | required | Message text, 1–4000 characters | | --idempotency-key | required | Stable key for this message; reuse on retries | | --question-seq | none | The `seq` of the question a suggested reply answers (1 or more); if a newer question was posted since, it answers none. Needed for "Find emails" at a `contacts_review` | ### prospecting status `discolike prospecting status RUN_ID` retrieves progress, chat messages, and one page of partial results. Use `next_event_seq` and `next_message_seq` as the next cursors. Responses include plan versions, in-flight batches, and the stable `saved_query_id` for saved results. | Flag | Default | Description | | --- | --- | --- | | --offset | 0 | Nonnegative result offset | | --limit | 100 | Page size, 1–500 | | --events-after | 0 | Return events after this nonnegative sequence | | --messages-after | 0 | Return messages after this nonnegative sequence | ### prospecting cancel `discolike prospecting cancel RUN_ID` stops new work. Partial results and incurred charges remain; submitted work can still drain. The run stays readable. The CLI has no delete command; use `client.prospecting.delete` in the SDK or `DELETE /prospecting/runs/{run_id}`. ### prospecting wait `discolike prospecting wait RUN_ID` returns the first result page at `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. Review and approve proposed plans before research starts. Inspect `status` and `stop_reason`; completion does not guarantee the target was reached. A timeout stops local polling only. At a [checkpoint](https://docs.discolike.com/api/endpoints/prospecting/#checkpoints), an interactive terminal shows the question, any sample companies, and numbered suggested replies, then sends your pick (a reply picked by number carries the question's `seq`) or your own text, and keeps waiting. Without a terminal, or with `--no-input`, it prints the run and a `needs_input` envelope with the question, suggested replies and `question_seq` on stderr, then exits with the needs-input exit code; answer with `prospecting message --text --question-seq ` and run `wait` again. | Flag | Default | Description | | --- | --- | --- | | --timeout | 3600 | Maximum local wait in seconds | | --poll-interval | 5 | Poll interval in seconds, at least five | | --no-input | false | Never prompt at a checkpoint; print the question and exit instead | --- # API Reference ## Authentication ## Base URL All API requests should be made to: ``` https://api.discolike.com/v1/ ``` ## OpenAPI Specification The complete API specification is available at [api.discolike.com/v1/openapi.json](https://api.discolike.com/v1/openapi.json). ## Creating an account from an agent An AI agent can create the account for its human without a browser: `POST /v1/public/signup` with the person's work email and name. The person confirms by email, logs in, and issues keys from the app. See the [Agent signup guide](https://docs.discolike.com/guides/agent-signup/) for per-agent instructions and the [Signup API](https://docs.discolike.com/api/endpoints/signup/) reference. ## API Keys API keys are the credential for servers, CI, and anything else without a browser. Create and manage your keys at [app.discolike.com/account/management/keys](https://app.discolike.com/account/management/keys). Include your API key in the `x-discolike-key` header: ```shell curl "https://api.discolike.com/v1/discover?keyword=fintech" \ -H "x-discolike-key: YOUR_API_KEY" ``` All API keys for the same account share rate limits. ## OAuth Bearer Tokens Every `/v1/*` route also accepts an OAuth 2.1 access token in the `Authorization` header. This is what the [CLI](https://docs.discolike.com/cli/#authentication) and [Python SDK](https://docs.discolike.com/sdk/#oauth-sessions) use after `discolike auth login`, and the same server the [MCP integration](https://docs.discolike.com/mcp/) authenticates against. ```shell curl "https://api.discolike.com/v1/discover?keyword=fintech" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` Tokens are issued by the authorization server described at [api.discolike.com/v1/.well-known/oauth-authorization-server](https://api.discolike.com/v1/.well-known/oauth-authorization-server), through the authorization-code flow with PKCE. Public clients register dynamically and use the `offline_access` scope to receive a refresh token. The protected-resource metadata at `api.discolike.com/.well-known/oauth-protected-resource/v1` names the resource the token must be issued for, `https://api.discolike.com/v1`, which is what a client passes as the `resource` parameter when authorizing and exchanging the code. Bearer support on the REST routes is live on the development environment and reaches production with the next platform release; before that, production accepts bearer tokens on `/v1/mcp` only. When more than one credential is present, the platform checks them in this order and uses the first that applies: an app web session, then the `Authorization: Bearer` header, then `x-discolike-key`. An invalid or expired bearer token returns `401` with `Invalid API Key or Session`; a valid token whose account is not a DiscoLike tenant returns `403`. ## Rate Limits Rate limits are requests per minute, counted per endpoint against a moving window. All API keys on one account share a bucket per endpoint. Going over returns `429 Too Many Requests` with a `Retry-After` header giving the seconds to wait. Separate [limits on work in progress](#limits-on-work-in-progress) cap queued email lookups and running async tasks. ## Limits by Endpoint | Endpoint | Method | Starter | Pro | Team | Company | Enterprise | |---|---|---|---|---|---|---| | [`/append`](https://docs.discolike.com/api/endpoints/append/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/bizdata`](https://docs.discolike.com/api/endpoints/bizdata/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/bulkmatch`](https://docs.discolike.com/api/endpoints/bulk-match/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/contacts`](https://docs.discolike.com/api/endpoints/contacts/) | `GET` | 5 | 10 | 15 | 25 | 100 | | [`/contacts/bulk-match`](https://docs.discolike.com/api/endpoints/contact-bulk-match/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/contacts/count`](https://docs.discolike.com/api/endpoints/contacts/) † | `GET` | 30 | 30 | 30 | 30 | 30 | | [`/contacts/discover`](https://docs.discolike.com/api/endpoints/contacts/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/contacts/lookup`](https://docs.discolike.com/api/endpoints/contacts/) † | `GET` | 30 | 30 | 30 | 30 | 30 | | [`/contacts/match`](https://docs.discolike.com/api/endpoints/contact-match/) † | `GET` | 10 | 10 | 10 | 10 | 10 | | [`/count`](https://docs.discolike.com/api/endpoints/count/) † | `GET` | 30 | 30 | 30 | 30 | 30 | | [`/crm/connections`](https://docs.discolike.com/api/endpoints/crm/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/crm/{provider}/authorize`](https://docs.discolike.com/api/endpoints/crm/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/crm/{provider}/connection`](https://docs.discolike.com/api/endpoints/crm/) | `DELETE` | 10 | 20 | 30 | 50 | 200 | | [`/crm/{provider}/suppress`](https://docs.discolike.com/api/endpoints/crm/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/discover`](https://docs.discolike.com/api/endpoints/discover/) | `GET` | 5 | 10 | 15 | 25 | 100 | | [`/email/batch/{batch_id}/results`](https://docs.discolike.com/api/endpoints/email-find/) | `GET` | 30 | 60 | 90 | 150 | 600 | | [`/email/find`](https://docs.discolike.com/api/endpoints/email-find/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/email/find/batch`](https://docs.discolike.com/api/endpoints/email-find/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/email/jobs/{job_id}`](https://docs.discolike.com/api/endpoints/email-find/) | `GET` | 30 | 60 | 90 | 150 | 600 | | [`/extract`](https://docs.discolike.com/api/endpoints/extract/) | `GET` | 5 | 10 | 15 | 25 | 100 | | [`/growth`](https://docs.discolike.com/api/endpoints/growth/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/llm-providers/config`](https://docs.discolike.com/api/endpoints/llm-providers/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/llm-providers/config`](https://docs.discolike.com/api/endpoints/llm-providers/) | `POST` | 10 | 20 | 30 | 50 | 200 | | [`/llm-providers/config/{integration_id}`](https://docs.discolike.com/api/endpoints/llm-providers/) | `DELETE` | 10 | 20 | 30 | 50 | 200 | | [`/llm-providers/config/{integration_id}`](https://docs.discolike.com/api/endpoints/llm-providers/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/llm-providers/config/{integration_id}`](https://docs.discolike.com/api/endpoints/llm-providers/) | `PUT` | 10 | 20 | 30 | 50 | 200 | | [`/llm-providers/config/{integration_id}/set-default`](https://docs.discolike.com/api/endpoints/llm-providers/) | `POST` | 10 | 20 | 30 | 50 | 200 | | [`/llm-providers/test-connection`](https://docs.discolike.com/api/endpoints/llm-providers/) | `POST` | 5 | 10 | 15 | 25 | 100 | | [`/match`](https://docs.discolike.com/api/endpoints/match/) | `GET` | 5 | 10 | 15 | 25 | 100 | | [`/publiclink`](https://docs.discolike.com/api/endpoints/publiclink/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/queries/exclusion-list`](https://docs.discolike.com/api/endpoints/queries/) | `POST` | 10 | 20 | 30 | 50 | 200 | | [`/queries/save-results`](https://docs.discolike.com/api/endpoints/queries/) | `POST` | 10 | 20 | 30 | 50 | 200 | | [`/redirects`](https://docs.discolike.com/api/endpoints/redirects/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/score`](https://docs.discolike.com/api/endpoints/score/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/search-providers`](https://docs.discolike.com/api/endpoints/search-providers/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/search-providers`](https://docs.discolike.com/api/endpoints/search-providers/) | `POST` | 10 | 20 | 30 | 50 | 200 | | [`/search-providers/models`](https://docs.discolike.com/api/endpoints/search-providers/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/search-providers/{integration_id}`](https://docs.discolike.com/api/endpoints/search-providers/) | `DELETE` | 10 | 20 | 30 | 50 | 200 | | [`/search-providers/{integration_id}`](https://docs.discolike.com/api/endpoints/search-providers/) | `PUT` | 10 | 20 | 30 | 50 | 200 | | [`/search-providers/{integration_id}/default`](https://docs.discolike.com/api/endpoints/search-providers/) | `DELETE` | 10 | 20 | 30 | 50 | 200 | | [`/search-providers/{integration_id}/default`](https://docs.discolike.com/api/endpoints/search-providers/) | `PUT` | 10 | 20 | 30 | 50 | 200 | | [`/segment`](https://docs.discolike.com/api/endpoints/segment/) | `GET` | 2 | 4 | 6 | 10 | 40 | | [`/segment`](https://docs.discolike.com/api/endpoints/segment/) | `POST` | 2 | 4 | 6 | 10 | 40 | | [`/subsidiaries`](https://docs.discolike.com/api/endpoints/subsidiaries/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/usage`](https://docs.discolike.com/api/endpoints/usage/) | `GET` | 20 | 40 | 60 | 100 | 400 | | [`/vendors`](https://docs.discolike.com/api/endpoints/vendors/) | `GET` | 10 | 20 | 30 | 50 | 200 | | [`/{task}/status/{task_id}`](https://docs.discolike.com/sdk/reference/#job-and-asyncjob) | `GET` | 60 | 120 | 180 | 300 | 1200 | † Unbilled. These consume no credits and the plan multiplier does not apply, so the cap is the same on every plan. ## Limits on Work in Progress Rate limits bound how fast you submit. Two more limits bound how much work one account can have queued or running at once, so one account's backlog cannot delay everyone else's. Both are counted per account at submit time and both answer `429 Too Many Requests` with `Retry-After: 60`. The `detail` message gives your current count and the limit. A refused request starts nothing and bills nothing; send it again once earlier work finishes. | Work | What counts | Starter | Pro | Team | Company | Enterprise | |---|---|---|---|---|---|---| | Email lookups | Lookups still queued or running | 2,000 | 4,000 | 6,000 | 10,000 | 40,000 | | Async tasks | Running DiscoGen, bulk match, contact bulk match and segment tasks | 3 | 3 | 3 | 3 | 3 | **Email lookups.** [`/email/find`](https://docs.discolike.com/api/endpoints/email-find/), `/email/find/batch` and the MCP `find-emails` tool check the limit. A batch that would take the account past it is refused whole, so a batch larger than the room left never starts partially. Lookups started by a ContaGen task, a prospecting run or the DiscoLike app count toward the same total, but those are never refused part way through. ```json { "detail": "Too many email lookups in progress: 1,900 queued or running, 500 more requested, limit 2,000 on your plan. Try again once some finish." } ``` **Async tasks.** Starting a fourth task while three are running is refused. This covers [`/discogen/process`](https://docs.discolike.com/api/endpoints/discogen/) and `/discogen/process/personas` (and retrying one of their tasks), [`/contacts/discover/generate`](https://docs.discolike.com/api/endpoints/contacts/), [`/validate/icp`](https://docs.discolike.com/api/endpoints/validate/), [`/bulkmatch`](https://docs.discolike.com/api/endpoints/bulk-match/), [`/contacts/bulk-match`](https://docs.discolike.com/api/endpoints/contact-bulk-match/), [`/segment`](https://docs.discolike.com/api/endpoints/segment/), and the MCP tools that start the same tasks. Wait for a task to finish or cancel one to free a slot. ```json { "detail": "3 tasks are already running (limit 3). Try again once one finishes, or cancel one from the Tasks page." } ``` Both counts are read when the request arrives, so requests sent at the same instant can briefly take an account slightly past a limit. The limits may be tuned over time; read the number from the `detail` message instead of hardcoding it. ## MCP MCP tool calls are rate limited per tool per user, mirroring the corresponding HTTP endpoint's limit: billed tools scale with the plan multiplier, and the unbilled tools `count-matching-domains`, `count-contacts`, `contact-lookup`, `match-contacts`, and `list-industry-categories` are flat at their base rate. A tool with no explicit rate gets 30 req/min scaled by the plan multiplier. See [MCP Tools](https://docs.discolike.com/mcp/tools/) for the tool list. ## Append API Enrich customer or prospect profiles by appending firmographic and domain data to a list of domains from a CSV or Excel file. Returns [BizData](https://docs.discolike.com/datasets/bizdata/) profiles, [Score](https://docs.discolike.com/api/endpoints/score/) values, and [Growth](https://docs.discolike.com/api/endpoints/growth/) metrics for each domain. ``` POST https://api.discolike.com/v1/append ``` ## Parameters Provide domains via a `file` upload, a `query_id` reference, or both. When using `file`, the request must use `Content-Type: multipart/form-data` with the file as the `file` parameter. Maximum file size is 1MB and 10,000 rows; larger files are rejected with a 400 error. (Changed: files over 10,000 rows were previously truncated to the first 10,000; they are now rejected outright.) If neither `file` nor `query_id` is provided, the request is rejected with a 400 error. | Parameter | Description | |-----------|-------------| | dataset | Datasets to append: `bizdata`, `domain_status`, `redirects`, `growth`, `vendors`, `subdomains`. Required, at least one value | | file | CSV/Excel file of domains (multipart form field). Optional if `query_id` is provided | | query_id | One or more saved-query IDs (from [`GET /queries/saved`](https://docs.discolike.com/api/endpoints/queries/)) to resolve into company domains, unioned with any domains from `file`. Accepts a comma-separated string or repeated values. Each saved query contributes up to its domain cap | | domain_column | Column containing domains. Auto-detected if not provided | ## Billing - `bizdata` bills net-new domains only. Domains retrieved in the last 90 days are cached and free. - The other datasets (`domain_status`, `redirects`, `growth`, `vendors`, `subdomains`) bill one record per row that returns data for that dataset; rows without data are free. There is no 90-day cache for these datasets, so re-running the same file bills them again. The request is priced before it runs at both request fees it can incur (BizData and the other datasets) and every row of every dataset requested; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest number of rows that fits. A dataset listed twice is priced and fetched once. ## Response Fields Field naming depends on the dataset combination: | Scenario | Bizdata fields | Non-bizdata fields | |----------|---------------|-------------------| | `bizdata` only | No prefix (`name`, `score`) | N/A | | `bizdata` + others | No prefix (`name`, `score`) | Prefixed (`redirects:redirect_count`) | | Single non-bizdata | N/A | No prefix (`status_code`) | | Multiple non-bizdata | N/A | Prefixed (`domain_status:status_code`, `growth:score_growth_3m`) | `domain` is always available without prefix. Original input columns are prefixed with `input:`. ### Dataset-specific fields | Dataset | Fields | |---------|--------| | `bizdata` | Standard [BizData](https://docs.discolike.com/api/endpoints/bizdata/) fields (`name`, `status`, `score`, `description`, etc.), plus the additional fields `employees`, `revenue_range`, `business_model`, `mx_provider`, `sub_industry` (see [Sub-Industries](https://docs.discolike.com/sub-industries/)), `latitude`, `longitude` and `geo_precision` | | `redirects` | `redirect_sources` (array of domains redirecting to this domain, capped at 1,000), `redirect_count` (true total) | | `domain_status` | `status_code`, `status_reason`, `record_date` | | `growth` | Quarterly `score_*` and `subdomains_*` fields, `score_growth_3m`, `subdomain_growth_3m` | | `vendors` | `vendors` (array of vendor/technology domains this company uses, capped at 100) | | `subdomains` | `subdomains` (array of subdomains observed for this domain, most popular first, capped at 300) | Returns JSON by default. Add `&csv=true` for CSV output. ## Examples ### Request ```shell curl --form file='@domains.csv' \ "https://api.discolike.com/v1/append?dataset=bizdata&dataset=redirects" \ -H "x-discolike-key: API_KEY" ``` Input file: ```csv domain acmesoftware.com ``` ### Response ```json [ { "domain": "acmesoftware.com", "name": "Acme Software Inc", "status": { "status": "active", "confidence": 1.0 }, "score": 250, "start_date": "2018-02-17", "address": { "state": "California", "country": "US" }, "latitude": 37.3382, "longitude": -121.8863, "geo_precision": "city", "phones": ["+15555550900"], "public_emails": ["info@acmesoftware.com"], "description": "Enterprise software company providing business intelligence and data analytics solutions.", "keywords": { "business intelligence": 0.63, "data analytics": 0.51 }, "industry_groups": { "SOFTWARE": 0.85 }, "sub_industry": { "SOFTWARE/CUSTOM_SOFTWARE_DEVELOPER": 0.62 }, "redirects:redirect_sources": ["oldacme.com", "acme-software.com"], "redirects:redirect_count": 2 } ] ``` Bizdata fields (`name`, `score`, `description`, etc.) are never prefixed. Non-bizdata fields are prefixed with the dataset name when combined with bizdata or other datasets. ## BizData API Returns firmographic data for a domain: company name, industry, location, languages, and social profiles. Employee count, revenue range, business model, sub-industry, coordinates, and MX provider are available as [additional fields](#additional-fields). Subdomains are automatically normalized to root domain. Use alongside [Score](https://docs.discolike.com/api/endpoints/score/) for lead prioritization and [Growth](https://docs.discolike.com/api/endpoints/growth/) for quarter-over-quarter changes. ``` GET https://api.discolike.com/v1/bizdata ``` The lookup is priced before it runs; when the remaining balance cannot cover one record, it is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Domain to look up | ## Response Fields | Field | Type | Description | |-------|------|-------------| | domain | String | Normalized domain (unique identifier) | | name | String | Company name from certificate or website | | status | Object | Operating status: `active` or `closed` with confidence score | | score | Integer | Digital footprint score (1-800) reflecting company size | | start_date | Date | First certificate date (company start estimate) | | end_date | Date | Last certificate date if closed, null if active | | address | Object | HQ address (street, city, state, zip, country). `state` is the ISO 3166-2 subdivision name, e.g. `California` | | phones | Array | Phone numbers from website | | public_emails | Array | Contact emails from website | | social_urls | Array | Social media profile URLs | | description | String | Company description from website | | keywords | Object | NLP-generated keywords with confidence scores | | industry_groups | Object | Top 2 industry classifications with scores (see [Industries](https://docs.discolike.com/industries/)) | | redirect_domain | String | Final domain if redirects exist | | update_date | Date | Last record update | ## Additional Fields | Field | Type | Description | |-------|------|-------------| | latitude | Float | Latitude of the company's location, returned right after `address`, when resolved | | longitude | Float | Longitude of the company's location, when resolved | | geo_precision | String | How precisely the coordinates were resolved: `rooftop`, `map_pin`, `street`, `zip`, `city`. Null when no coordinates were resolved | | sub_industry | Object | Second-level industry labels as `PARENT/SUB`:confidence pairs, returned right after `industry_groups`. Null when the domain was never scored against its category's sub-labels, `{}` when it was scored and nothing matched. See [Sub-Industries](https://docs.discolike.com/sub-industries/) for all 217 labels | | employees | String | Employee count range: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+` | | revenue_range | String | Revenue bucket: `<1M`, `1-10M`, `10-100M`, `100M-1B`, `>1B`, or `N/A` when unknown | | business_model | Object | Business model labels with confidence scores: `B2B`, `B2C`, `B2G`, `G2B`, `G2C`, `D2C`, `C2C`, `C2B` | | mx_provider | String | Email/MX hosting provider domain (e.g., `google.com`, `microsoft.com`), `no_mx` when the domain has no mail server, or `null` when not resolved | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/bizdata?domain=acmecorp.com" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "domain": "acmecorp.com", "name": "Acme Corporation", "status": { "status": "active", "confidence": 1 }, "score": 650, "start_date": "2010-03-15", "end_date": null, "address": { "street": "123 Main Street", "city": "San Francisco", "state": "California", "zip": "94105", "country": "US" }, "latitude": 37.7897, "longitude": -122.3972, "geo_precision": "rooftop", "phones": ["+15555550100"], "public_emails": ["info@acmecorp.com"], "social_urls": [ "https://twitter.com/acmecorp", "https://www.linkedin.com/company/acme-corporation" ], "description": "Leading provider of enterprise software solutions.", "keywords": { "enterprise software": 0.70, "cloud solutions": 0.65, "business automation": 0.61 }, "industry_groups": { "SOFTWARE": 0.95, "IT_SERVICES": 0.01 }, "sub_industry": { "SOFTWARE/CUSTOM_SOFTWARE_DEVELOPER": 0.68 }, "employees": "201-500", "revenue_range": "10-100M", "business_model": { "B2B": 0.92, "B2G": 0.08 }, "mx_provider": "google.com", "update_date": "2024-01-15" } ``` ## Contacts API The Contacts API enables searching for business contacts/personas using vector search, with support for similarity matching, filtering, and name-based lookup. The primary endpoint is `POST /v1/contacts/discover`. The legacy `GET /v1/contacts` endpoint is still functional and documented at the end of this page for existing integrations. Every request is priced before it runs at its request fee plus `max_records` contacts and the companies they can come from (all assumed new to you); one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest `max_records` (or `max_companies`, when you set it) that fits. ## Discover Contacts ### Endpoint ``` POST https://api.discolike.com/v1/contacts/discover ``` Search for business contacts and return them **grouped by company domain**, with per-company firmographic data on each entry. Billed additively: net-new contacts plus net-new firmo domains for any company surfaced for the first time in the 90-day cache window. The two counters share the same credit pool. ### Request Body JSON body. Every parameter documented below is accepted as a JSON field. ### Search Parameters | Parameter | Description | | --------- | ----------- | | persona_id | Use one or more persona IDs as the basis for similarity matching. Up to 10 IDs can be provided. | | icp_prompt | Natural language ICP description. Automatically extracts structured contact filters (seniority, department, industry, country, employee/revenue range, title keywords) and cleans the semantic description. When the query requires company-level signals (tech stack, funding, product descriptions), automatically chains to company discovery to find matching companies first, then searches contacts there. One parameter does everything, and is preferred over setting individual filters manually. | | icp_text | Natural language description of your ideal contact profile for semantic matching. Does NOT extract filters. Use `icp_prompt` instead for automatic extraction. | | domain | Limit results to contacts at specific domains. | | inclusion_query_id | Include results from saved queries. Works with domain lists (scopes to companies) and contact lists (scopes to specific contacts). | | exclusion_query_id | Exclude results from saved queries. Works with domain lists (excludes companies) and contact lists (excludes specific contacts). | ### Persona Filters | Parameter | Description | | --------- | ----------- | | seniority | Filter by seniority level. Acceptable values: `executive`, `vp`, `director`, `manager`, `senior_ic`, `mid_level`, `entry_level` | | negate_seniority | Exclude contacts with specified seniority levels. | | department | Filter by department. Must be one of the 15 canonical values exactly (case-sensitive): `Operations`, `Executive`, `Technology`, `Sales - Marketing`, `Finance`, `Legal`, `Human Resources`, `Medical - Science`, `Customer Service`, `Research & Development`, `Administration`, `Public Relations`, `Investor Relations`, `Pro Services`, `Other`. Any other value returns 422. Use `icp_prompt` if you want natural-language extraction (e.g. "engineering leaders" → `Technology`). | | negate_department | Exclude contacts in specified departments. Same 15 canonical values as `department`. | | skills | Filter by skills. Multiple skills can be provided. | | name | Filter by contact name (partial match supported). | | title | Filter by job title. Each item is a separate match term. Supports quoted phrases and `+` prefix for required terms. C-suite acronyms are auto-expanded to their spelled-out forms and vice versa (e.g. `CEO` also matches `Chief Executive Officer`); wrap a term in quotes to match it literally without expansion. | | negate_title | Exclude contacts with specified job titles. C-suite acronyms are expanded the same way as in `title`. | | summary | Match profile summary text: every word of any one term, in any order. Quote a multi-word term to keep it together, prefix + to require it. | | negate_summary | Exclude contacts whose profile summary contains every word of any one term, in any order. | | person_country | Filter by contact's country using ISO-3166-1 alpha-2 codes. Also accepts region codes: `ANZ`, `APAC`, `ASEAN`, `BENELUX`, `CEE`, `DACH`, `EMEA`, `EU`, `GCC`, `LATAM`, `MENA`, `NORDICS`, and the continents `EUROPE`, `ASIA`, `AFRICA`, `NORTH_AMERICA`, `SOUTH_AMERICA`, `OCEANIA`, `ANTARCTICA`. | | negate_person_country | Exclude contacts in specified countries. | | person_state | Filter by contact's state/region. Accepts ISO 3166-2 codes (e.g. `CA`, `BY`) or full names (e.g. `California`, `Bayern`). Resolved against every selected `person_country` (region aliases are expanded first), so multiple countries are allowed and an ambiguous name is kept for each country it resolves under. A value that resolves under none of them is dropped rather than rejected. See [States](https://docs.discolike.com/states/) for supported countries. | ### Contact Data Filters | Parameter | Description | | --------- | ----------- | | has_email | Set to `true` to only include contacts with email addresses. | | ~~email_validated~~ | **Deprecated.** Treated as `has_email`: contact search no longer filters on email verification; verify emails instead. `true` returns contacts that have an email address, and every result comes back unverified (`email_validated: null`). Use `has_email` in new requests. | | has_phone | Set to `true` to only include contacts with phone numbers. | | has_mobile | Set to `true` to only include contacts whose phone array contains at least one mobile number. | | has_linkedin | Set to `true` to only include contacts with LinkedIn profiles. | | min_connections | Minimum LinkedIn connections required. | | jobstart_date | Filter by job start date: a minimum date (`YYYY-MM-DD`) or a range (`YYYY-MM-DD,YYYY-MM-DD`). Matches contacts who started their current role in this window, the "recently hired" filter. Contacts without a known start date are excluded. | ### Company Filters | Parameter | Description | | --------- | ----------- | | filter_industry | Filter by company industry. Uppercase enum values (e.g. `SAAS`, `SOFTWARE`, `FINANCIAL_SERVICES`, `HEALTHCARE`, `CLOUD_COMPUTING`). See [BizData Dataset](https://docs.discolike.com/datasets/bizdata/) for the full enum. | | negate_filter_industry | Exclude contacts at companies in specified industries. | | filter_country | Filter by company country using ISO-3166-1 alpha-2 codes. Also accepts the same region aliases as `person_country`. | | negate_filter_country | Exclude contacts at companies in specified countries. | | filter_state | Filter by company state/region. Accepts ISO 3166-2 codes (e.g. `CA`, `BY`) or full names (e.g. `California`, `Bayern`). Resolved against every selected `filter_country` (region aliases are expanded first), so multiple countries are allowed and an ambiguous name is kept for each country it resolves under. A value that resolves under none of them is dropped rather than rejected. See [States](https://docs.discolike.com/states/) for supported countries. | | negate_filter_state | Exclude contacts at companies in specified states (same format as `filter_state`). | | employee_range | Filter by company size. Format: `'min,max'` (e.g. `'51,200'`). Maps to buckets: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+`. | | ~~revenue_range~~ | **Deprecated, no-op.** Revenue is a company-level attribute. Filter on [/discover](https://docs.discolike.com/api/endpoints/discover/) by `revenue_range` and pass the resulting domains to /contacts/discover. The output field is still populated on each result. | ### Pagination & Output | Parameter | Description | | --------- | ----------- | | max_records | Maximum contacts to return, range 20-10,000 (optional, defaults to 100). Use this for a literal total-contact cap. Do not combine with `max_companies`. | | max_companies | Maximum enriched companies to return, range 1-10,000. Use this when you want the response to stop after up to N company/domain entries. Do not combine with `max_records`. When paired with `results_by_company=0` (no per-company cap), the total is capped at 10,000 contacts. | | offset | Records to skip for pagination (optional, defaults to 0). | | results_by_company | Max contacts per company domain, range 0-100 (optional, defaults to 5). With `max_companies`, controls how many contacts can be returned under each enriched company. Example: `max_companies=100, results_by_company=2` → up to 100 companies with up to 2 contacts each. Set `results_by_company=0` to remove the per-company cap. With `max_records`, the total is capped at `max_records`; with `max_companies`, the total is capped at 10,000. Any non-zero value forces `offset` to 0. | | include_search_contacts | Set to `true` to include input persona_ids in results (optional, defaults to false). | | consensus | Number of top results to use for building search vector, range 1-20 (optional, defaults to 1). | Use either `max_companies` or `max_records`, not both. `max_companies` caps the number of enriched company/domain entries in the grouped response. `max_records` caps the total number of contact rows. Use `max_records` with `offset` to paginate through large result sets. For example, to fetch 500 contacts in two pages: - Page 1: `max_records=250, offset=0, results_by_company=0` - Page 2: `max_records=250, offset=250, results_by_company=0` `max_companies` always returns from the first page of matching enriched companies. Any non-zero `results_by_company` also forces `offset` to 0, so set `results_by_company=0` with `max_records` if you need to page through a large result set. ### Example Request ```shell curl -X POST https://api.discolike.com/v1/contacts/discover \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "icp_prompt": "VP of Sales at SaaS companies", "filter_industry": ["SAAS"], "max_companies": 50, "results_by_company": 5 }' ``` ### Example Response ```json { "results": { "stripe.com": { "domain": "stripe.com", "name": "Stripe", "employees": "10001+", "revenue_range": ">1B", "industry_groups": {"SAAS": 0.81, "SOFTWARE": 0.1}, "address": {"country": "US", "state": "California"}, "email_pattern": "{first_name}@stripe.com", "email_pattern_confidence": 59, "email_pattern_guess": "marissa@stripe.com", "contacts": [ { "persona_id": 319675651, "domain": "stripe.com", "name": "Marissa Fagnani", "title": "Delivery Manager", "department": "Technology", "seniority": "manager", "skills": ["program management", "delivery"], "phone": [ {"phone": "+17406072462", "type": "mobile"}, {"phone": "+14197337789", "type": "mobile"} ], "email": "marissa@stripe.com", "email_validated": null, "social_urls": ["https://www.linkedin.com/in/marissa-fagnani"], "connections": 500, "country": "US", "state": "OR", "industry": ["SAAS", "SOFTWARE"], "employees": "10001+", "revenue_range": ">1B", "jobstart_date": "2025-11-01", "similarity": 87, "summary": null, "company_name": "Stripe" } ] } }, "total_contacts": 142, "total_domains": 38 } ``` ### Field Notes - The company-level `address.state` is the ISO 3166-2 subdivision **name** (e.g. `California`), while the persona-level `state` on each contact is the raw subdivision **code** (e.g. `OR`). Both appear in the same response. - `phone` is an **array of objects**: `[{"phone": "+E164", "type": "mobile" | "landline"}, ...]`. A contact can have multiple entries. Type is re-derived per call from libphonenumber, so it is always populated when a phone is present. - `industry` is an **array of uppercase enum strings**, e.g. `["SAAS", "SOFTWARE"]`. See [BizData Dataset](https://docs.discolike.com/datasets/bizdata/) for the full enum. - `department` is one of the 15 values listed under [Persona Filters](#persona-filters). - `social_urls` may include LinkedIn and X/Twitter URLs. - `email_validated` is the result of an email verification run on the contact: `true` if the address was verified, `false` if the check failed, `null` if it has not been verified. Contact search never sets it, so it is always `null` here, and `email` is not proven deliverable. Verify addresses with the Verify action in the DiscoLike app, or find a proven address for a name with [Email Find](https://docs.discolike.com/api/endpoints/email-find/). ### Search Modes The endpoint supports three modes, selected by which parameters you provide: 1. **Similarity mode**: when `persona_id`, `icp_text`, or `icp_prompt` is provided. Uses vector search with cosine similarity scores (0-100 scale on the `similarity` field). 2. **Domain mode**: when only `domain` is provided. Returns all contacts at the specified domains matching filters. 3. **Filter mode**: when no search input is provided. Returns contacts matching the company and persona filters. ## Discover Contacts: BYOK Generate ### Endpoint ``` POST https://api.discolike.com/v1/contacts/discover/generate ``` Discovers candidate contacts at a list of companies using **your own search provider** (BYOS) and either **your own LLM** (BYOK) or **DiscoLike Groove, the native extractor** (no LLM key). Runs through the [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/) pipeline with web search always enabled. Returns a task ID immediately; poll `GET /discogen/status/{task_id}` for per-domain contact rows plus the company's inferred email pattern. A search provider is required either way. Without one the request returns `400`. No platform billing per contact returned, and the request count is the same for both engines. Results may include hallucinated personas, so treat `email`, `linkedin_url`, and other identifying fields as **candidate data** until validated. ### Engines `integration_id` picks the engine: | Value | Engine | Behavior | |-------|--------|----------| | An LLM integration UUID | Your LLM (BYOK) | Extracts contacts and validates each title against `icp_text`. Status reports `title_validation: "llm"` | | `"native"` | DiscoLike Groove (`discolike/groove`) | No LLM key needed. Extracts every person the search surfaces at the company together with a title. No title validation against `icp_text`, so every row comes back with `title_match_status: "match"`. Status reports `title_validation: "none"` | | Omitted | DiscoLike Groove when it is available and a search provider resolves; otherwise your default LLM integration | Matches whichever engine it resolves to. Pass an LLM integration UUID to have titles validated | `icp_text` still shapes the search queries on the native engine, so it stays worth writing well. Native titles are best effort: filter them yourself when titles must match the ICP exactly. The other DiscoGen endpoints accept UUIDs only. Sending `"native"` to them returns `400`. ### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | icp_text | String | Yes | Persona description (e.g., "VPs or Directors of Marketing at B2B SaaS"). Must be non-empty | | domains | Array | Yes | List of company domains to search at (1-10,000) | | context_mode | String | No | What data the LLM sees: `website` (default), `profile`, `domain` | | integration_id | String | No | LLM provider integration UUID, or `native` for keyless extraction without title validation. Omit for DiscoLike Groove when it is available and a search provider resolves (your default, or your only one), otherwise your default LLM integration | | search_provider_id | String | No | Search provider UUID. Uses your default, or your only one, if omitted. Required at the org level | | search_context_size | String | No | Search depth per company: `low` (default), `medium`, `high` | | max_contacts_per_domain | Integer | No | Cap on contacts returned per company (default 10) | | find_emails | Boolean | No | Default `false`. When `true`, every row that has a name but no `email` is run through the [email finder](https://docs.discolike.com/api/endpoints/email-find/) before the task completes: `email` is filled where an address was proven and `email_status` carries the finder verdict. Found addresses bill under the finder's rules (90-day dedupe); nothing else on the run bills. The task is priced at submit with every contact a domain can yield counted as a find; before submitting the finds it prices them against the live balance again and, when they do not fit, skips them with a warning and keeps the contacts with `email` null | Web search is always enabled server-side, because contact discovery depends on LinkedIn/company-site lookups. Without `find_emails`, `email` is null unless a source shows the address verbatim, so the task returns candidate names, titles and LinkedIn URLs. To find addresses afterwards instead, build `first_name,last_name,domain` rows from the result and submit them to `POST /email/find/batch`. ### Response ```json { "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "column_name": ["name", "title", "department", "seniority", "email", "linkedin_url", "skills", "phone", "email_pattern", "email_pattern_confidence", "email_pattern_guess"], "status": "in_progress", "total_domains": 2 } ``` ### Result Schema (per domain, multi-row) Each domain yields `{"contacts": [...], "email_pattern": ..., "email_pattern_confidence": ..., "email_pattern_guess": ...}`: zero or more contact rows plus the company-wide email pattern, once per domain (pattern fields are omitted when the indexer has none): | Column | Type | Notes | |--------|------|-------| | name | String | Full name of the contact | | title | String\|null | Job title | | department | String\|null | One of the 15 canonical department values (see [Persona Filters](#persona-filters)) | | seniority | String\|null | `executive`, `vp`, `director`, `manager`, `senior_ic`, `mid_level`, `entry_level` | | email | String\|null | Only if a source shows it verbatim, or proven by the finder when `find_emails` is set | | email_status | String\|null | Only with `find_emails`: the finder verdict for a row it was run on: `found`, `not_found`, `catch_all`, `catch_all_pattern`, `undeliverable`, `inconclusive`, `ambiguous`, `timeout`, `greylisted`, `rate_limited`, `error`. Absent on rows that already had an email or had no usable name | | linkedin_url | String\|null | LinkedIn profile URL | | skills | Array\|null | Skills attributed to the contact | | phone | Array | Array of phone objects with `phone` (`+E164`) and `type` (`mobile` or `landline`) fields, same shape as the indexed `/contacts/discover` response. | | email_pattern | String\|null | Template, e.g. `first.last@`, `flastname@` | | email_pattern_confidence | Number\|null | 0-1 | | email_pattern_guess | String\|null | Example email for a hypothetical John Smith | ### Example ```bash curl -X POST https://api.discolike.com/v1/contacts/discover/generate \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "icp_text": "VPs or Directors of Marketing at B2B SaaS", "domains": ["gusto.com", "rippling.com"] }' ``` Run the same list on the native engine, with no LLM key: ```bash curl -X POST https://api.discolike.com/v1/contacts/discover/generate \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "icp_text": "VPs or Directors of Marketing at B2B SaaS", "domains": ["gusto.com", "rippling.com"], "integration_id": "native" }' ``` Poll for results: ```bash curl https://api.discolike.com/v1/discogen/status/TASK_ID \ -H "x-discolike-key: API_KEY" ``` Each visible contact also carries `title_match_status` (`match` or `unsure`), preserving the extraction verdict. Treat `unsure` as unresolved; native extraction still requires independent role checking even when it emits `match`. The completed status payload carries `title_validation`, `"llm"` or `"none"`, telling you whether titles were validated against `icp_text`. ## Count Contacts ### Endpoint ``` GET https://api.discolike.com/v1/contacts/count ``` Accepts the same filter parameters as the Discover Contacts endpoint (persona filters, contact data filters, and company filters). Useful for sizing a result set before paying to retrieve it. ### Example Request ```shell curl "https://api.discolike.com/v1/contacts/count?seniority=vp&filter_industry=SAAS&filter_country=US" \ -H "x-discolike-key: API_KEY" ``` ### Example Response ```json { "count": 15432 } ``` ## Lookup Contact ### Endpoint ``` GET https://api.discolike.com/v1/contacts/lookup ``` ### Parameters | Parameter | Description | | --------- | ----------- | | persona_id | The persona ID to look up. | | linkedin | LinkedIn URL or username to look up. Must be a valid LinkedIn profile URL or a bare username. | | email | Email address to look up (exact match, case-insensitive). Must be a valid email address. | One of `persona_id`, `linkedin`, or `email` must be provided. Returns `404` when no contact matches. ### Example Request ```shell curl "https://api.discolike.com/v1/contacts/lookup?persona_id=12345678" \ -H "x-discolike-key: API_KEY" ``` Or lookup by LinkedIn URL/username: ```shell curl "https://api.discolike.com/v1/contacts/lookup?linkedin=https://www.linkedin.com/in/janedoe" \ -H "x-discolike-key: API_KEY" ``` Or lookup by email: ```shell curl "https://api.discolike.com/v1/contacts/lookup?email=jane.doe@example.com" \ -H "x-discolike-key: API_KEY" ``` ### Example Response ```json { "persona_id": 12345678, "name": "Jane Doe", "title": "VP of Sales", "domain": "example.com", "company_name": "Example Corp", "summary": "Experienced sales leader with 15+ years in enterprise software..." } ``` ## Legacy: Search Contacts (GET) `GET /v1/contacts` predates the contacts API restructure. It still works, but it is hidden from the OpenAPI schema and new integrations should use `POST /v1/contacts/discover` above. The legacy endpoint returns a flat list of contacts rather than the domain-grouped shape from `/contacts/discover`. ### Endpoint ``` GET https://api.discolike.com/v1/contacts ``` Same filter vocabulary as `POST /v1/contacts/discover`, passed as query parameters instead of a JSON body. For repeated values (e.g., multiple domains, seniority levels), repeat the query parameter: `?domain=a.com&domain=b.com&seniority=vp&seniority=director`. For result limits, pass either `max_companies` or `max_records`, not both. ### Example Request ```shell curl "https://api.discolike.com/v1/contacts?icp_text=VP%20of%20Sales%20at%20SaaS%20companies&seniority=vp&filter_industry=SAAS&max_companies=50&results_by_company=5" \ -H "x-discolike-key: API_KEY" ``` ### Example Response ```json [ { "persona_id": 319675651, "domain": "stripe.com", "name": "Marissa Fagnani", "title": "Delivery Manager", "department": "Technology", "seniority": "manager", "skills": ["program management", "delivery"], "phone": [ {"phone": "+17406072462", "type": "mobile"}, {"phone": "+14197337789", "type": "mobile"} ], "email": "marissa@stripe.com", "email_validated": null, "social_urls": ["https://www.linkedin.com/in/marissa-fagnani"], "connections": 500, "country": "US", "state": "OR", "industry": ["SAAS", "SOFTWARE"], "employees": "10001+", "revenue_range": ">1B", "jobstart_date": "2025-11-01", "similarity": 87 } ] ``` ## Contact Match API Match a person name to contact profiles. Returns ranked candidates with match scores. ``` GET https://api.discolike.com/v1/contacts/match ``` ## Parameters | Parameter | Description | | --------- | ----------- | | name | Person name to search for (required). Must be at least 2 characters after whitespace and punctuation are stripped. | | company_name | Company name to narrow search (optional). | | domain | Domain to filter by (optional). | | person_country | Person's country code to filter by (optional). | | limit | Maximum results to return, range 1-20 (optional, defaults to 10). | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/contacts/match?name=John%20Smith&company_name=Acme%20Corp&limit=5" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "query": { "name": "Jane Doe", "company_name": "Acme Corp", "domain": null }, "matches": [ { "persona_id": 12345678, "name": "Jane Doe", "title": "VP of Sales", "domain": "acmecorp.com", "company_name": "Acme Corporation", "match_score": 95.2 } ] } ``` ## Contact Bulk Match API Match a list of person names to contacts in bulk. Uses text search for fast matching, with optional full-data enrichment from the contacts database. ``` POST https://api.discolike.com/v1/contacts/bulk-match ``` With `enrich`, the task is priced before it is queued at the request fee plus `limit` records per query; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest number of queries that fits at that `limit`. ## Parameters | Parameter | Type | Description | |-----------|------|-------------| | queries | Array | List of match queries (required, 1-500 items). Each item has: `name` (optional), `email` (optional), `linkedin` (optional LinkedIn profile URL or username), `company_name` (optional), `domain` (optional), `person_country` (optional ISO-3166-1 alpha-2 code). At least one of `name`, `email` or `linkedin` is required per item. | | enrich | Boolean | Set to `true` to hydrate matches with full contact data from the contacts database. Counts toward usage. Defaults to `false`. | | limit | Integer | Maximum matches per query, range 1-20 (optional, defaults to 10). | Queries with a `linkedin` URL or an `email` are looked up by exact match first, LinkedIn before email (returned with `match_score` 100, and full contact data when `enrich` is `true`); on a miss they fall back to the other key, then to name/company matching. LinkedIn values can be full profile URLs (`https://www.linkedin.com/in/jane-doe`), URLs without the scheme (`linkedin.com/in/jane-doe`) or bare usernames; a value that is not a profile URL is ignored for that row. ## Examples ### Request ```shell curl -X POST "https://api.discolike.com/v1/contacts/bulk-match" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "queries": [ {"name": "Jane Doe", "company_name": "Acme Corp"}, {"name": "John Smith", "domain": "techstartup.io"}, {"email": "jane.doe@acmecorp.com"}, {"linkedin": "https://www.linkedin.com/in/jane-doe"} ], "enrich": false, "limit": 5 }' ``` ## Initial Response ```json { "task_id": "e362cb75-35e2-4006-a239-3d87a7472872" } ``` An account can have three DiscoGen, bulk match, contact bulk match and segment tasks running at once. Starting another answers `429` with `Retry-After: 60` until one finishes or is cancelled; see [limits on work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress). ## Check Status ```shell curl "https://api.discolike.com/v1/contactmatch/status/{task_id}" \ -H "x-discolike-key: API_KEY" ``` ```json { "status": "in_progress", "progress": 45 } ``` ## Final Response When `enrich` is `false`, matches contain lightweight results (persona_id, name, title, domain, company_name, match_score): ```json { "status": "completed", "progress": 100, "results": [ { "query": { "name": "Jane Doe", "company_name": "Acme Corp", "domain": null }, "matches": [ { "persona_id": 12345678, "name": "Jane Doe", "title": "VP of Sales", "domain": "acmecorp.com", "company_name": "Acme Corporation", "match_score": 95.2 } ] } ] } ``` When `enrich` is `true`, matches contain full contact data (email, phone, social URLs, seniority, department, skills, etc.), the same fields as Search Contacts results. ## Count API Get a count of companies matching your search criteria before running a full [Discover](https://docs.discolike.com/api/endpoints/discover/) query. Use this to preview result volume and refine filters before committing to a full search. ``` GET https://api.discolike.com/v1/count ``` ## Parameters ### Search Filters | Parameter | Description | |-----------|-------------| | phrase_match | Exact text to search (up to 20 phrases, each at least 3 characters) | | negate_phrase_match | Exact text to exclude (up to 20, each at least 3 characters) | | tech_stack | Vendor domains to include (up to 20) | | negate_tech_stack | Vendor domains to exclude (up to 20) | | subdomain | Limit results to these subdomains (up to 20, each at least 3 characters) | | negate_subdomain | Subdomains to exclude (up to 20, each at least 3 characters) | ### Location Filters | Parameter | Description | |-----------|-------------| | country | ISO-3166-1 alpha-2 country codes (e.g., US, GB, DE). Also accepts region codes that auto-expand to member countries: `ANZ`, `APAC`, `ASEAN`, `BENELUX`, `CEE`, `DACH`, `EMEA`, `EU`, `GCC`, `LATAM`, `MENA`, `NORDICS`, and the continents `EUROPE`, `ASIA`, `AFRICA`, `NORTH_AMERICA`, `SOUTH_AMERICA`, `OCEANIA`, `ANTARCTICA`. | | negate_country | Countries to exclude | | state | State/province filter. Accepts ISO 3166-2 codes (e.g. `CA`, `BY`) or full names (e.g. `California`, `Bayern`). Requires exactly one `country` value: passing more than one rejects `state`. That single value may be a region alias (e.g. `EU`, `APAC`, `DACH`), in which case states are resolved against every country the alias expands to. Supported for 120+ countries; see [States](https://docs.discolike.com/states/). | | negate_state | States to exclude (same format and country rule as `state`, up to 100) | | geo | Circular search area written `lat,lon` or `lat,lon,radius` (e.g. `30.27,-97.74`, `30.27,-97.74,30mi`). Repeatable: pass it once per circle. The radius is a number optionally suffixed with `km` or `mi`, a bare number meaning kilometres; it defaults to 50km and may not exceed 1000km | | bbox | Bounding box written `min_lat,min_lon,max_lat,max_lon`. Repeatable: pass it once per box. Longitudes may wrap the antimeridian by setting `min_lon` above `max_lon` | | lat | Latitude of a search centre. Must be supplied together with `lon` | | lon | Longitude of a search centre. Must be supplied together with `lat` | | radius | Search radius around `lat`/`lon`: a number optionally suffixed with `km` or `mi` (`50`, `50km`, `30mi`). A bare number is kilometres. Defaults to 50km, maximum 1000km | Every `geo` circle, every `bbox` and the circle formed by `lat`/`lon`/`radius` are OR'd together, up to 10 shapes in total, and the combined area is AND'd with the rest of the filters. ### Company Filters | Parameter | Description | |-----------|-------------| | min_digital_footprint | Minimum score (0-800, default: 0 — counts include every score unless you set a floor). Must not exceed `max_digital_footprint`. Capped at the highest score reachable by a company founded on `start_date` | | max_digital_footprint | Maximum score (0-800, default: 800) | | start_date | Company start date (`YYYY-MM-DD` or range `YYYY-MM-DD,YYYY-MM-DD`) | | category | Industry filter (see [Industries](https://docs.discolike.com/industries/)) | | negate_category | Industries to exclude | | sub_industry | Second-level industry label, scoped to a category. Accepts a bare label (`ROOFING`) or a qualified key (`CONSTRUCTION/ROOFING`), case-insensitive, up to 50 values. A bare label whose parent is unambiguous adds that parent to `category`. A category with one of its sub-industries selected is narrowed to them; any other category stays whole (`category=SAAS,CONSTRUCTION&sub_industry=HOME_BUILDER` returns SaaS companies and home builders). See [Sub-Industries](https://docs.discolike.com/sub-industries/) for all 217 values. | | negate_sub_industry | Sub-industries to exclude. Same label format, but validated for taxonomy membership only: it never adds to or is checked against `category`, and a bare label that exists under several categories excludes it under all of them. | | employee_range | Employee count range as `min,max` (e.g., `1,5000`). Buckets: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+ | | revenue_range | Company revenue range as `min,max` in raw integers (e.g., `1000000,10000000`). Use `+` on max for unbounded. Buckets: `<1M`, `1-10M`, `10-100M`, `100M-1B`, `>1B`. `N/A` swept in when range covers `<1M` | | business_model | Business-model labels: B2B, B2C, B2G, G2B, G2C, D2C, C2C, C2B (OR semantics) | | negate_business_model | Business-model labels to exclude | ### Other Filters | Parameter | Description | |-----------|-------------| | social | Required social presence: facebook, linkedin, twitter, instagram, etc. | | negate_social | Social profiles to exclude | | language | Site language 2-letter codes (see [Languages](https://docs.discolike.com/languages/)) | | negate_language | Languages to exclude | | redirect | Include redirecting domains (0 or 1, default: 0) | | exclude_leadgen | Exclude suspected lead generation sites; filters out low-score profiles that also lack phone, email, and social media presence (default: false — unlike Discover, counts include them unless you opt in) | ## Validation Count applies the same limits as Discover for the parameters it shares; see the [Discover validation table](https://docs.discolike.com/api/endpoints/discover/#validation). Malformed `employee_range` or `revenue_range` values are silently dropped rather than rejected. ## Response Fields | Field | Type | Description | |-------|------|-------------| | count | Integer | Number of matching profiles | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/count?phrase_match=enterprise+software&country=US" \ -H "x-discolike-key: API_KEY" ``` Sizing several metro areas at once: ```shell curl "https://api.discolike.com/v1/count?category=CONSTRUCTION&geo=30.27,-97.74,30mi&geo=32.78,-96.80,30mi" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "count": 1542 } ``` ## CRM API [Beta] Connect a CRM to DiscoLike and keep discovery aware of what is already in your pipeline. Supported providers: `hubspot`, `pipedrive`, `salesforce`. These endpoints are in **beta**: flows are live end-to-end, but expect changes while hardening completes. The sync operations (import, match, write-back, audit, contact enrichment) are not part of the public REST surface. Run them from the app under **Settings > Integrations**, or through the [MCP CRM tools](https://docs.discolike.com/mcp/crm/). ``` GET https://api.discolike.com/v1/crm/connections GET https://api.discolike.com/v1/crm/{provider}/authorize POST https://api.discolike.com/v1/crm/{provider}/suppress DELETE https://api.discolike.com/v1/crm/{provider}/connection ``` `{provider}` is one of `hubspot`, `pipedrive`, `salesforce`. An unknown provider returns `404`. ## List Connections `GET /crm/connections` returns the CRM connections for your organization. | Parameter | Description | |-----------|-------------| | probe | Optional. `true` live-checks each connection against the provider and returns a fresh status instead of the last stored one. Default `false`. | | Field | Type | Description | |-------|------|-------------| | provider | String | `hubspot`, `pipedrive`, or `salesforce` | | status | String | `active` or `needs_reauth` | | connected_at | String | ISO timestamp of when the connection was created, or `null` | ```shell curl "https://api.discolike.com/v1/crm/connections?probe=true" \ -H "x-discolike-key: API_KEY" ``` ```json [ { "provider": "hubspot", "status": "active", "connected_at": "2026-05-14T09:12:44+00:00" } ] ``` A `needs_reauth` status means the stored token no longer works. Reconnect from the app before running any CRM operation. ## Authorize `GET /crm/{provider}/authorize` mints a provider OAuth authorize URL bound to a single-use state token that expires after 10 minutes. Send the user to `authorize_url`; the provider redirects back to DiscoLike, which stores the connection. | Field | Type | Description | |-------|------|-------------| | authorize_url | String | Provider OAuth URL to open in a browser | ```shell curl "https://api.discolike.com/v1/crm/hubspot/authorize" \ -H "x-discolike-key: API_KEY" ``` ```json { "authorize_url": "https://app.hubspot.com/oauth/authorize?response_type=code&client_id=..." } ``` ## Suppress `POST /crm/{provider}/suppress` snapshots the connected CRM into a saved exclusion list, so companies or people already in your pipeline stop surfacing in discovery. Returns `202` immediately; the snapshot runs as a background job and the list appears under [Saved Queries](https://docs.discolike.com/api/endpoints/queries/) when it finishes. | Parameter | Description | |-----------|-------------| | kind | `companies` (default) excludes by domain. `contacts` resolves the CRM's contact emails against the DiscoLike contact index and excludes the matching people. | | Field | Type | Description | |-------|------|-------------| | message | String | Confirmation that the snapshot was queued | | provider | String | Provider snapshotted | | kind | String | `companies` or `contacts` | ```shell curl -X POST "https://api.discolike.com/v1/crm/hubspot/suppress?kind=companies" \ -H "x-discolike-key: API_KEY" ``` ```json { "message": "CRM snapshot queued", "provider": "hubspot", "kind": "companies" } ``` CRM contacts whose email DiscoLike has never seen are skipped; they cannot appear in results anyway. Pass the resulting query's ID as `exclusion_query_id` on [Discover](https://docs.discolike.com/api/endpoints/discover/) or [Contacts](https://docs.discolike.com/api/endpoints/contacts/) to apply the suppression. ## Disconnect `DELETE /crm/{provider}/connection` removes the connection and its integration record. Requires an **admin app session**, not an API key. Run it from **Settings > Integrations** in the app. | Field | Type | Description | |-------|------|-------------| | message | String | Confirmation that the connection was deleted | Returns `404` if there is no connection for that provider. ## Rate Limits These endpoints carry their own per-minute limits, scaled by plan tier on top of the [standard multipliers](https://docs.discolike.com/api/rate-limits/). | Endpoint | Base limit | |----------|------------| | `GET /crm/connections` | 20 / min | | `GET /crm/{provider}/authorize` | 10 / min | | `POST /crm/{provider}/suppress` | 5 / min | | `DELETE /crm/{provider}/connection` | 10 / min | ## DiscoGen API DiscoGen is an AI research agent that runs your prompt against rich company or contact data at scale. Each record gets its own isolated context window (company profile, homepage text, or contact details), so the model sees focused, accurate information rather than a polluted batch. Optionally enable web search for live data enrichment. Requires an [LLM provider](https://docs.discolike.com/api/endpoints/llm-providers/) integration. Optionally pair with a [search provider](https://docs.discolike.com/api/endpoints/search-providers/) integration for BYOS (Bring Your Own Search) web search. ``` POST https://api.discolike.com/v1/discogen/process POST https://api.discolike.com/v1/discogen/process-personas GET https://api.discolike.com/v1/discogen/models GET https://api.discolike.com/v1/discogen/status/{task_id} DELETE https://api.discolike.com/v1/discogen/cancel/{task_id} ``` A task is priced when it is submitted, at the request fee plus every input domain or persona (a persona counts twice when `context_mode` is `company` or `full`, since its company is billed too); one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest number of domains or personas that fits. While it runs, each batch is priced against the live balance first: a task whose balance runs out stops before the next batch, keeps and bills the rows it finished, and reports the balance in its error. ## Parameters Both `/process` and `/process-personas` share these parameters: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | query | String | Yes | The prompt to run against each record. Must be non-empty | | integration_id | String | No | LLM provider integration UUID. Uses your default LLM provider, or your only one, if omitted; with several and none marked default, omitting it returns `400`. Must be a valid UUID, or `native-icp` on `/process` to score an ICP validation prompt with DiscoLike's own model (see [Native ICP-Fit Scoring](#native-icp-fit-scoring)) | | web_search | Boolean | No | Enable web search enrichment (default: `false`). Returns 400 if no search provider is available and the model lacks built-in web search | | search_provider_id | String | No | Search provider integration UUID for BYOS web search. Uses your default search provider, or your only one, if omitted. Must be a valid UUID | | search_context_size | String | No | Number of search queries per record: `low` (default), `medium`, or `high` | | include_x_search | Boolean | No | Include X (Twitter) search results for xAI models (default: `false`) | | typed_columns | Boolean | No | Answer yes/no, fixed-set, and scale parts of your query with a calibrated judgment model instead of generated text (default: `false`). See [Typed Columns](#typed-columns) | | include_confidence | Boolean | No | Add a confidence column beside each typed column (default: `false`). No effect without `typed_columns: true` | ## Process Domains Run an LLM prompt against each domain with company context. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | domains | Array | Yes | Domains to process (1-10,000) | | context_mode | String | No | What data the model sees per domain (default: `website`) | | previous_discogen_data | Object | No | Previous DiscoGen results keyed by domain, appended to context for iterative enrichment | ### Domain Context Modes Controls what data is included in each domain's context window. Richer context produces better results but costs more. Credits are only charged for new domains; any domain you've already discovered in the last 90 days is free. | Mode | Context Included | Cost | |------|-----------------|------| | `website` | Company profile (firmographics, keywords, categories) **+** homepage text | Credits (new domains only) + model fees | | `profile` | Company profile only (firmographics, keywords, categories, summary) | Credits (new domains only) + model fees | | `domain` | Domain name only; model relies on its own knowledge and web search if enabled | Model fees only | ### Example Request ```shell curl -X POST "https://api.discolike.com/v1/discogen/process" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "What products or services does this company offer?", "domains": ["acmecorp.com", "globex.com"], "context_mode": "website" }' ``` ### Example Response ```json { "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "column_name": "Products and Services", "status": "in_progress", "total_domains": 2 } ``` ### Native ICP-Fit Scoring `/process` also accepts `integration_id: "native-icp"`, which scores each domain with DiscoLike's own ICP-fit model instead of your LLM provider. Your `query` must be an ICP validation prompt listing its criteria under `Mandatory:`, `Reject if:` and `Nice-to-have:`; the model answers nothing else. [Validate](https://docs.discolike.com/api/endpoints/validate/) generates such a prompt for you from plain ICP text, and is the easier entry point. The run needs no LLM provider integration and makes no calls on your provider key. `web_search` is ignored, `context_mode` is always `website`, and `column_name` is `["ICP Fit", "ICP Score", "Reasoning"]` rather than a label detected from your prompt, with `Reasoning` carrying the confidence band rather than a rationale, because this engine scores rather than explains. Column values and errors are documented under [Native ICP-Fit Model](https://docs.discolike.com/api/endpoints/validate/#native-icp-fit-model). ## Process Personas Run an LLM prompt against contact records. Same async task model as domain processing. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | persona_ids | Array | Yes | Persona IDs to process (1-10,000) | | context_mode | String | No | What data the model sees per persona (default: `profile`) | | previous_discogen_data | Object | No | Previous results keyed by persona ID for iterative enrichment | ### Persona Context Modes Credits are only charged for new contacts; any contact you've already discovered in the last 90 days is free. | Mode | Context Included | Cost | |------|-----------------|------| | `full` | Contact profile + employer company profile + company homepage text | Credits (new contacts only) + model fees | | `company` | Contact profile + employer company profile | Credits (new contacts only) + model fees | | `profile` | Full contact profile (title, department, seniority, skills) | Credits (new contacts only) + model fees | | `profile_summary` | Condensed contact profile, a shorter summary of the full profile | Credits (new contacts only) + model fees | | `name_only` | Contact name and LinkedIn URL only; model relies on its own knowledge and web search | Model fees only | ### Example Request ```shell curl -X POST "https://api.discolike.com/v1/discogen/process-personas" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "Is this person likely a decision-maker for software purchases?", "persona_ids": [12345, 67890], "context_mode": "full" }' ``` ## Typed Columns By default every column in a DiscoGen result is generated prose from your LLM provider. `typed_columns: true` lets part of a query be answered as a calibrated judgment instead: a yes/no, one of a fixed set, or a position on an ordered scale. Those columns are answered by DiscoLike's TypeSafe (Jev) judgment model rather than your LLM provider, and come back with a probability behind the scenes, not just a label. There is no separate schema to define. You send `typed_columns: true` and DiscoGen's detector reads your `query` text to decide which parts of it are judgments and which are prose — the query is the whole specification. For example: ``` "Does this company offer a free trial? What pricing model do they use (Freemium, Subscription, or One-time)? Describe their target customer in one sentence." ``` detects a yes/no column, a fixed-set column, and a free-text column in a single run, and answers the first two with TypeSafe while your LLM provider writes the third. Requesting a typed column requires a TypeSafe integration on your account (see [LLM Providers: TypeSafe](https://docs.discolike.com/api/endpoints/llm-providers/#typesafe-integrations)), independent of whichever `integration_id` the rest of the query runs on. Without one, the request 400s with `TYPESAFE_INTEGRATION_MISSING` before anything is queued. If the integration is removed while a run is in progress, the remaining typed cells come back as `"Error: No TypeSafe integration is configured; add one to use typed columns"` rather than failing the row or the run. Other failures render as `"Error: "` in the same way — a rejected key reads `"Error: JevRequestError"`. Generated columns in the same run are unaffected. Typed yes/no cells contain `"Yes"` or `"No"`. Fixed-set cells contain the chosen option. Scale cells contain a numeric, probability-weighted position on the detected ordered scale: `0` is the first level, `1` the second, and fractional values fall between levels. The level descriptions are in `response_format.typed_columns[].criteria`. ### Confidence Add `include_confidence: true` to get a ` confidence` column beside each typed column, one of: | Value | Meaning | |-------|---------| | `High confidence` | Strong signal in the evidence | | `Medium confidence` | Moderate signal | | `Low confidence - the judgment is near its cutoff, treat it as unresolved` | Near the decision boundary; treat as unresolved rather than trusting the label | ### Typed-Only Runs (TypeSafe as the Engine) Pass a TypeSafe integration's UUID as `integration_id` to run the query typed-only: every column is answered by TypeSafe and no LLM provider is called at all, so the run makes no calls on an LLM key and needs no LLM provider integration. `column_name` becomes the typed columns' labels, and anything the query would otherwise generate as prose is dropped rather than left unanswered. - `integration_id` must be picked explicitly. If a TypeSafe integration is resolved by falling back to your organization's default, the request 400s with `TYPESAFE_NOT_A_GENERATIVE_ENGINE` — a TypeSafe integration cannot be set as the org default in the first place (see [LLM Providers](https://docs.discolike.com/api/endpoints/llm-providers/#validation)), but an existing default can still end up pointing at one if its provider is edited afterward, so this is enforced on every request. - If the query carries no judgment at all (nothing the detector reads as yes/no, fixed-set, or scale), the request 400s with `TYPESAFE_NO_TYPED_COLUMNS` — rephrase it as a judgment, or select an LLM integration instead. ### Example Request ```shell curl -X POST "https://api.discolike.com/v1/discogen/process" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "Does this company offer a free trial? What pricing model do they use (Freemium, Subscription, or One-time)?", "domains": ["acmecorp.com", "globex.com"], "typed_columns": true, "include_confidence": true }' ``` ### Example Result ```json { "status": "completed", "results": { "acmecorp.com": { "Free Trial": "Yes", "Free Trial confidence": "High confidence", "Pricing Model": "Subscription", "Pricing Model confidence": "Medium confidence" } }, "response_format": { "response_type": "free_text", "field_labels": {}, "typed_columns": [ { "key": "free_trial", "label": "Free Trial", "kind": "noul", "instructions": "Whether the company offers a free trial of its product", "criteria": { "true": "The website advertises a free trial or trial period", "false": "The website does not advertise a free trial or trial period" } }, { "key": "pricing_model", "label": "Pricing Model", "kind": "choice", "instructions": "The company's primary pricing model", "criteria": { "Freemium": "A free tier exists alongside paid plans", "Subscription": "Access requires a recurring paid plan, no free tier", "One-time": "A single upfront purchase, no recurring billing" } } ] } } ``` Judgment values are returned under their column labels in `results` (or `interim_results` while the task is running), alongside any generated fields. For a generated list of rows, the judgments repeat on every row for that company or contact. Cancelled runs retain the answers that finished. `response_format.typed_columns` echoes the detected columns, including the `criteria` TypeSafe judged each one against, so you can see why a value came back the way it did. A column is never listed in both places: `field_labels`/`json_schema` cover only the generative columns (empty on a typed-only run), and `typed_columns` covers only the judgment columns. Jev spend is not billed to any provider key you configure; it is folded into `estimated_cost` but is not broken out as its own line in `cost_metadata`, which only tracks your LLM provider and search provider calls. ## List Models Returns available LLM models grouped by provider, with web search support flags, and the regions each regional provider accepts (`provider_regions`; pick one with `#region=` in an integration's [model_name](https://docs.discolike.com/api/endpoints/llm-providers/#regions-and-accounts)). ```shell curl "https://api.discolike.com/v1/discogen/models" \ -H "x-discolike-key: API_KEY" ``` ```json { "models": { "openai": [ { "name": "gpt-5-mini", "supports_web_search": true }, { "name": "gpt-5-nano", "supports_web_search": true }, { "name": "gpt-4o", "supports_web_search": false }, { "name": "gpt-4o-mini", "supports_web_search": false } ], "anthropic": [ { "name": "claude-sonnet-4-6", "supports_web_search": false } ] }, "provider_regions": { "bedrock": ["ap-northeast-1", "eu-central-1", "us-east-1", "us-west-2"], "watsonx": ["au-syd", "ca-tor", "eu-de", "eu-gb", "jp-tok", "us-south"] } } ``` ## Task Lifecycle Both process endpoints return a task that can be polled and cancelled. An account can have three DiscoGen, bulk match, contact bulk match and segment tasks running at once. Starting another answers `429` with `Retry-After: 60` until one finishes or is cancelled; see [limits on work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress). ### Check Status ```shell curl "https://api.discolike.com/v1/discogen/status/{task_id}" \ -H "x-discolike-key: API_KEY" ``` ```json { "status": "in_progress", "progress": 45, "interim_results": { "acmecorp.com": "Enterprise cloud security platform offering..." }, "estimated_cost": 0.0032 } ``` ### Final Response ```json { "status": "completed", "progress": 100, "results": { "acmecorp.com": "Enterprise cloud security platform offering threat detection, SIEM, and compliance tools.", "globex.com": "Industrial supply chain management with logistics optimization and warehouse automation." }, "estimated_cost": 0.0058, "response_format": { "response_type": "free_text", "description": "Products and services description", "field_labels": { "answer": "Products and Services" } }, "warnings": [], "cost_metadata": { "openai/gpt-4o-mini": { "calls": 2, "search_calls": 0, "prompt_tokens": 6140, "completion_tokens": 212, "est_cost_usd": 0.001048 }, "search_provider": { "provider": "serper", "search_model": "serper/search", "queries_executed": 4, "queries_succeeded": 4, "est_cost_usd": 0.004 } } } ``` ### Cost Metadata and Warnings `estimated_cost` and `cost_metadata` describe spend on your own provider keys (LLM and search). They are best-effort estimates for display, not DiscoLike billing: providers may hide cached-token usage, and custom endpoints cannot be priced. `cost_metadata` has one entry per `provider/model` the run called, plus a `search_provider` entry when a search provider was used: | Key | Description | |-----|-------------| | `calls` | LLM calls made with this model | | `search_calls` | Searches performed by the model's **built-in** search tool (OpenAI, xAI, Gemini grounding). Always `0` when a search provider is configured, because the provider supplies the search context and built-in search is turned off | | `prompt_tokens`, `completion_tokens` | Token usage reported by the provider; absent if the provider did not report it | | `est_cost_usd` | Estimated spend for that model, or for the search provider | | `search_provider.queries_executed` | Search queries sent to your search provider (BYOS) across the whole run | | `search_provider.queries_succeeded` | Queries that returned a response | Read `search_provider.queries_executed`, not `search_calls`, to confirm that web search ran on a BYOS run. ### Title Validation Completed contact generation runs also carry `title_validation`: `"llm"` when your LLM validated each contact's title against `icp_text`, `"none"` when the native extractor ran and titles were not validated. The field is absent on regular DiscoGen and persona runs. See [Discover Contacts: BYOK Generate](https://docs.discolike.com/api/endpoints/contacts/#discover-contacts-byok-generate). `warnings` lists non-fatal problems that changed how the run was grounded: the search provider running out of credits or tripping its circuit breaker, or `web_search: true` with a search provider configured that never executed a query. Results still complete, but affected rows are grounded only in profile and website text. The list is deduplicated and keeps the most recent entries. ### Cancel Task ```shell curl -X DELETE "https://api.discolike.com/v1/discogen/cancel/{task_id}" \ -H "x-discolike-key: API_KEY" ``` ```json { "success": true, "message": "Task cancellation requested", "task_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "cancelling" } ``` ## Discover API Find companies matching your criteria using domain lookalike matching, natural language ICP descriptions, phrase matching, or structured filters. Returns [BizData](https://docs.discolike.com/datasets/bizdata/) profiles ranked by relevance. Use [Count](https://docs.discolike.com/api/endpoints/count/) to preview result volume before running a full query, and [Segment](https://docs.discolike.com/api/endpoints/segment/) to group results into ICP clusters. ``` GET https://api.discolike.com/v1/discover ``` Every request is priced before it runs at its request fee plus `max_records` records (all assumed new to you); one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest `max_records` that fits. ## Discovery Methods The discover endpoint supports multiple ways to find companies. You can combine them for hybrid search. | Method | Parameter | Description | |--------|-----------|-------------| | ICP Prompt | `icp_prompt` | Describe your ICP in natural language; automatically extracts filters, generates ICP text, suggests domains | | Domain Lookalike | `domain` | Provide example domains to find similar companies | | ICP Text | `icp_text` | Describe your ideal customer in natural language | | Filters Only | *(structured filters)* | Use location, industry, score, and other filters without a search vector | ## Parameters ### Discovery Input | Parameter | Type | Description | |-----------|------|-------------| | domain | string[] | Domain(s) for lookalike matching (up to 10) | | negate_domain | string[] | Domains to push results away from (up to 10) | | icp_text | string | Natural language description of your ideal customer profile (3-4000 characters) | | negate_icp_text | string | Description of business concepts to exclude (3-4000 characters) | ### Phrase Matching | Parameter | Type | Description | |-----------|------|-------------| | phrase_match | string[] | Exact text fragments to match in site content (up to 20, each at least 3 characters) | | negate_phrase_match | string[] | Exact text fragments to exclude (up to 20, each at least 3 characters) | ### Location Filters | Parameter | Type | Description | |-----------|------|-------------| | country | string[] | ISO-3166-1 alpha-2 country codes (e.g., US, GB, DE). Also accepts region codes that auto-expand to member countries: `ANZ`, `APAC`, `ASEAN`, `BENELUX`, `CEE`, `DACH`, `EMEA`, `EU`, `GCC`, `LATAM`, `MENA`, `NORDICS`, and the continents `EUROPE`, `ASIA`, `AFRICA`, `NORTH_AMERICA`, `SOUTH_AMERICA`, `OCEANIA`, `ANTARCTICA`. | | negate_country | string[] | Countries to exclude | | state | string[] | State/province filter. Accepts ISO 3166-2 codes (e.g. `CA`, `BY`) or full names (e.g. `California`, `Bayern`). Requires exactly one `country` value: passing more than one rejects `state`. That single value may be a region alias (e.g. `EU`, `APAC`, `DACH`), in which case states are resolved against every country the alias expands to. Supported for 120+ countries; see [States](https://docs.discolike.com/states/). Up to 100 values, each must resolve to a known subdivision. | | negate_state | string[] | States to exclude (same format and country rule as `state`, up to 100) | | geo | string[] | Circular search area written `lat,lon` or `lat,lon,radius` (e.g. `30.27,-97.74`, `30.27,-97.74,30mi`). Repeatable: pass it once per circle. The radius is a number optionally suffixed with `km` or `mi`, a bare number meaning kilometres; it defaults to 50km and may not exceed 1000km | | bbox | string[] | Bounding box written `min_lat,min_lon,max_lat,max_lon`. Repeatable: pass it once per box. Longitudes may wrap the antimeridian by setting `min_lon` above `max_lon` | | lat | float | Latitude of a search centre. Must be supplied together with `lon` | | lon | float | Longitude of a search centre. Must be supplied together with `lat` | | radius | string | Search radius around `lat`/`lon`: a number optionally suffixed with `km` or `mi` (`50`, `50km`, `30mi`). A bare number is kilometres. Defaults to 50km, maximum 1000km | Every `geo` circle, every `bbox` and the circle formed by `lat`/`lon`/`radius` are OR'd together: a domain matches if it falls inside any one of them. A query may carry up to 10 shapes in total. The combined area is then AND'd with the rest of the filters, `country` and `state` included. ``` ?geo=30.27,-97.74,10km&geo=52.52,13.405&bbox=51.2,-0.5,51.7,0.3 ``` ### Company Filters | Parameter | Type | Description | |-----------|------|-------------| | min_digital_footprint | integer | Minimum digital footprint score (0-800, default: 25). Must not exceed `max_digital_footprint`. When `start_date` is set, the floor is capped at the highest score a company of that age can reach (see [Score methodology](https://docs.discolike.com/api/endpoints/score/#methodology)) | | max_digital_footprint | integer | Maximum digital footprint score (0-800, default: 800) | | start_date | string | Company start date: `YYYY-MM-DD` or range `YYYY-MM-DD,YYYY-MM-DD` | | category | string[] | Industry filter (e.g. SOFTWARE, HEALTHCARE). See [Industries](https://docs.discolike.com/industries/) for all 53 values. | | negate_category | string[] | Industries to exclude | | sub_industry | string[] | Second-level industry label, scoped to a category. Accepts a bare label (`ROOFING`) or a qualified key (`CONSTRUCTION/ROOFING`), case-insensitive, up to 50 values. A bare label whose parent is unambiguous adds that parent to `category`. A category with one of its sub-industries selected is narrowed to them; any other category stays whole (`category=SAAS,CONSTRUCTION&sub_industry=HOME_BUILDER` returns SaaS companies and home builders). See [Sub-Industries](https://docs.discolike.com/sub-industries/) for all 217 values. | | negate_sub_industry | string[] | Sub-industries to exclude. Same label format, but validated for taxonomy membership only: it never adds to or is checked against `category`, and a bare label that exists under several categories excludes it under all of them. | | employee_range | string | Employee count range as `min,max` (e.g., `1,5000`). Open-ended forms: `50,` means "50 and above", `,50` means "50 and below". Buckets: 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001-10000, 10001+ | | revenue_range | string | Company revenue range as `min,max` in raw integers (e.g., `1000000,10000000` for $1M-$10M). Use `+` suffix on max for unbounded (`1000000000+` for $1B+). Buckets: `<1M`, `1-10M`, `10-100M`, `100M-1B`, `>1B`. Domains with unknown revenue (`N/A`) are included when the range covers the `<1M` bucket. | | business_model | string[] | Business-model labels (OR semantics). Values: `B2B`, `B2C`, `B2G`, `G2B`, `G2C`, `D2C`, `C2C`, `C2B` | | negate_business_model | string[] | Business-model labels to exclude | ### Social & Language Filters | Parameter | Type | Description | |-----------|------|-------------| | social | string[] | Require social presence: facebook, instagram, linkedin, twitter, x, youtube, tiktok, pinterest, threads, yelp, googleplay, applestore, amazon, vk, bluesky, xing | | negate_social | string[] | Exclude companies with specified social profiles | | language | string[] | Filter by site language (2-letter codes: en, de, fr, ja, etc.). See [Languages](https://docs.discolike.com/languages/) for the full list. | | negate_language | string[] | Languages to exclude | | redirect | boolean | Include domains that redirect to another domain (default: false) | | exclude_leadgen | boolean | Exclude suspected lead generation sites; filters out low-score profiles that also lack phone, email, and social media presence (default: true) | ### Tech Stack Targeting | Parameter | Type | Description | |-----------|------|-------------| | tech_stack | string[] | Filter to companies using specified vendor domains (up to 20) | | negate_tech_stack | string[] | Exclude companies using specified vendor domains (up to 20) | ### Subdomain Targeting | Parameter | Type | Description | |-----------|------|-------------| | subdomain | string[] | Limit results to specified subdomains (up to 20) | | negate_subdomain | string[] | Exclude specified subdomains (up to 20) | ### Query Inclusion | Parameter | Type | Description | |-----------|------|-------------| | inclusion_query_id | string[] | Limit to domains from saved query results | ### Query Exclusion | Parameter | Type | Description | |-----------|------|-------------| | exclusion_query_id | string[] | Exclude domains from saved query results | | exclude_domain | string[] | Hard-exclude specific domains from results (up to 100). Accepts comma-separated (`a.com,b.com`) or repeated params. Unlike `negate_domain`, does not affect lookalike matching. | ### Result Controls | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | max_records | integer | 100 | Maximum records to return (5-10,000) | | offset | integer | 0 | Records to skip for pagination (0-10,000). To go deeper, exclude what you already have with `exclusion_query_id` and start again from 0 | | min_similarity | integer | 0 | Minimum similarity score (0-99) | | consensus | integer | 1 | **Deprecated, no-op.** Discover applies consensus itself, only to single-domain lookalike searches without ICP text | | relevance | string | | Where a lookalike list stops: results follow the similarity ranking until the estimated ICP fit of further companies drops too low, so the list ends at the edge of the real market. MAX_RELEVANCE gives the shortest, tightest list. BALANCED sits in the middle and is the app's default. MAX_COVERAGE gives the longest list and maps more of the market, with more borderline companies near the end. Omit it for no automatic stop: results run to `max_records`. Applies to lookalike searches driven by `icp_text`, `domain`, or both. The first page of a stopped list returns at least the top 200 rows when that many match. A stopped list can return an empty later page, which means the market edge was reached | | variance | string | UNRESTRICTED | **Deprecated.** Still accepted: LOW, MID_LOW, MEDIUM act as MAX_RELEVANCE; MID_HIGH, HIGH as BALANCED; UNRESTRICTED as no stop. `relevance` wins when both are sent | | include_search_domains | boolean | false | Include input domains in results | ### AI Features | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | enhanced | boolean | false | **Deprecated, no-op.** Discover no longer adds AI-suggested domains. A seed domain whose site cannot be read is described automatically | | retrieval | boolean | false | Enable page data retrieval using Extract API | | auto_icp_text | boolean | false | Auto-generate ICP text from provided domain(s) | | auto_phrase_match | boolean | false | Auto-generate phrase matches from ICP text | | icp_prompt | string | | Natural language ICP description. Auto-extracts filters, generates cleaned ICP text, suggests lookalike domains, and runs discovery in one call. Overrides auto_icp_text and auto_phrase_match. Max 4000 characters | ## Validation Limits enforced on every request. [Count](https://docs.discolike.com/api/endpoints/count/) applies the same limits to the parameters it shares. | Rule | Parameters | |------|------------| | 3-4000 characters. An empty string is treated as if the parameter were omitted | `icp_text`, `negate_icp_text` | | Max 4000 characters | `icp_prompt` | | Max 10 items | `domain`, `negate_domain` | | Max 20 items, each at least 3 characters | `phrase_match`, `negate_phrase_match`, `subdomain`, `negate_subdomain` | | Max 20 items | `tech_stack`, `negate_tech_stack` | | Max 100 items | `exclude_domain` | | Max 100 items, each must resolve to an ISO 3166-2 subdivision code or name, and only with a single `country` value, which may be a region alias covering several countries | `state`, `negate_state` | | Max 50 items, each must be a known sub-industry label, and its parent category must be in `category` when `category` is non-empty | `sub_industry` | | Max 50 items, each must be a known sub-industry label; no relationship to `category` is enforced | `negate_sub_industry` | | Four finite numbers `min_lat,min_lon,max_lat,max_lon` with `-90` ≤ `min_lat` < `max_lat` ≤ `90` and both longitudes within `-180` to `180` and not equal | `bbox` | | `min_digital_footprint` must not exceed `max_digital_footprint` | `min_digital_footprint`, `max_digital_footprint` | | Max 10 shapes in total, counting every `geo` circle, every `bbox` and the `lat`/`lon` centre | `geo`, `bbox`, `lat`, `lon` | | `lat` and `lon` must be supplied together, and `radius` requires both | `lat`, `lon`, `radius` | | Greater than 0 and at most 1000km | `radius`, the radius inside a `geo` value | | `YYYY-MM-DD`, or `min,max` in the same format | `start_date` | | 5-10,000 | `max_records` | | 1-20 | `consensus` | | 0-99 | `min_similarity` | | 0-10,000 | `offset` | | One of the listed values | `relevance`, `variance` | | Unknown parameters are rejected | all | `employee_range` and `revenue_range` are the exception: a value that does not parse is silently dropped and the query runs without the filter. ## Response Headers | Header | Description | |--------|-------------| | X-Total-Count | Number of results returned | | X-Applied-Filters | JSON object containing the filters actually used for the search. When `icp_prompt` is provided, this shows the extracted and merged filters (category, country, employee_range, tech_stack, phrase_match, icp_text, domain, etc.). Useful for understanding what the system interpreted from your prompt and for building iterative refinement workflows. `geo` and `bbox` are echoed as arrays of strings, one entry per shape, even when a single value was sent | ## Response Fields Returns an array of [BizData](https://docs.discolike.com/datasets/bizdata/) profiles with additional discovery fields: | Field | Type | Description | |-------|------|-------------| | similarity | float | Similarity score (0-100) between result and query | | latitude | float | Latitude of the company's location, when resolved. Returned right after `address` | | longitude | float | Longitude of the company's location, when resolved | | geo_precision | string | How precisely the coordinates were resolved: `rooftop`, `map_pin`, `street`, `zip`, `city`. Null when no coordinates were resolved | | sub_industry | object \| null | Sub-industry labels as `PARENT/SUB`:confidence pairs, returned right after `industry_groups`. `null` means the domain was never scored against its category's sub-labels; `{}` means it was scored and nothing matched. See [Sub-Industries](https://docs.discolike.com/sub-industries/) for all 217 labels. | | employees | string | Employee count range bucket | | vendors | string[] | Technology vendors detected (when tech_stack filter is used) | See the [BizData dataset](https://docs.discolike.com/datasets/bizdata/) for the full profile schema. ## Examples ### Domain Lookalike ```shell curl "https://api.discolike.com/v1/discover?domain=stripe.com&country=US&min_digital_footprint=100&max_records=50" \ -H "x-discolike-key: API_KEY" ``` ### ICP Text Search ```shell curl "https://api.discolike.com/v1/discover?icp_text=enterprise+SaaS+companies+in+financial+services&country=US&min_digital_footprint=200" \ -H "x-discolike-key: API_KEY" ``` ### ICP Prompt (One-Call Discovery) ```shell curl "https://api.discolike.com/v1/discover?icp_prompt=SaaS+companies+in+financial+services+with+50-200+employees+in+the+US&min_digital_footprint=200" \ -H "x-discolike-key: API_KEY" ``` ### Multiple Geographic Areas ```shell curl "https://api.discolike.com/v1/discover?category=CONSTRUCTION&geo=30.27,-97.74,30mi&geo=32.78,-96.80,30mi&bbox=29.6,-95.7,30.1,-95.0" \ -H "x-discolike-key: API_KEY" ``` ### Phrase Match with Filters ```shell curl "https://api.discolike.com/v1/discover?phrase_match=cloud+security&category=CYBERSECURITY&employee_range=51,5000" \ -H "x-discolike-key: API_KEY" ``` ### Sub-Industry Within a Radius ```shell curl "https://api.discolike.com/v1/discover?sub_industry=ROOFING&lat=30.27&lon=-97.74&radius=50mi&max_records=50" \ -H "x-discolike-key: API_KEY" ``` ### Companies Inside a Bounding Box ```shell curl "https://api.discolike.com/v1/discover?category=SOFTWARE&bbox=37.2,-122.6,37.9,-122.0&max_records=50" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json [ { "domain": "acmesoftware.com", "name": "Acme Software", "similarity": 87.3, "status": { "status": "active", "confidence": 1 }, "score": 450, "start_date": "2015-03-20", "address": { "city": "San Francisco", "state": "California", "country": "US" }, "latitude": 37.7749, "longitude": -122.4194, "geo_precision": "rooftop", "phones": ["+15555550200"], "public_emails": ["info@acmesoftware.com"], "social_urls": ["https://linkedin.com/company/acme-software"], "redirect_domain": null, "description": "Enterprise software solutions for modern businesses.", "keywords": { "enterprise software": 0.72, "cloud platform": 0.65 }, "industry_groups": { "SOFTWARE": 0.89 }, "sub_industry": { "SOFTWARE/CUSTOM_SOFTWARE_DEVELOPER": 0.71 }, "employees": "201-500", "mx_provider": "google.com" } ] ``` ## Email Find API Find a person's work email address from their first name, last name, and company domain. DiscoLike enumerates candidate patterns for the domain and SMTP-verifies them, returning the proven address plus the pattern it matched. Jobs run asynchronously: submit returns immediately with a job or batch id, then poll for results. Submit one person per call, or up to 500 per batch. Only proven addresses (status `found`) are billed, as records at your plan's per-1K rate, deduplicated per account for 90 days. Catch-all results, pattern guesses, and failed lookups are free. ``` POST https://api.discolike.com/v1/email/find POST https://api.discolike.com/v1/email/find/batch GET https://api.discolike.com/v1/email/jobs/{job_id} GET https://api.discolike.com/v1/email/batch/{batch_id}/results ``` A submission is priced on its number of lookups, as if every one were found; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) before anything is queued. ## Parameters Single find (`POST /email/find`): | Parameter | Type | Description | |-----------|------|-------------| | first_name | String | First name of the person (required) | | last_name | String | Last name of the person (required) | | domain | String | Target email domain, e.g. `acmecorp.com` (required) | | known_pattern | String | Known email local-part pattern for this domain (optional) | Batch find (`POST /email/find/batch`): | Parameter | Type | Description | |-----------|------|-------------| | requests | Array | List of single-find objects as above (required, 1-500 items) | | source_query_id | String | Saved query id whose stored contacts receive these verdicts (optional; App-internal linkage, safe to omit) | | refs | Array | Contact row ids, one per request, aligned by index (optional; requires `source_query_id`) | | round | String | `verify` (default) or `escalate` (optional; App-internal linkage, safe to omit) | ## Examples ### Request ```shell curl -X POST "https://api.discolike.com/v1/email/find/batch" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "requests": [ {"first_name": "Jane", "last_name": "Doe", "domain": "acmecorp.com"}, {"first_name": "John", "last_name": "Smith", "domain": "techstartup.io"} ] }' ``` ## Initial Response Single find returns a job id; batch find returns a batch id plus the job ids it created: ```json { "batch_id": "7f6f0a4e-52f7-4f3e-9c2d-0b8d3f1c9a11", "job_ids": ["e362cb75-35e2-4006-a239-3d87a7472872", "b91d2c40-8f1e-47f5-a6c3-2d7e9c0f4b22"], "total": 2 } ``` A request that would take your account past its limit of queued or running email lookups is refused whole with `429` and `Retry-After: 60`; see [limits on work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress). ## Poll Results ```shell curl "https://api.discolike.com/v1/email/batch/{batch_id}/results" \ -H "x-discolike-key: API_KEY" ``` Keep polling until `completed + failed` equals `total`. Single jobs are polled at `/email/jobs/{job_id}` and carry the same per-job shape as the batch `results` items. ## Final Response ```json { "batch_id": "7f6f0a4e-52f7-4f3e-9c2d-0b8d3f1c9a11", "total": 2, "completed": 2, "failed": 0, "results": [ { "job_id": "e362cb75-35e2-4006-a239-3d87a7472872", "status": "completed", "kind": "find", "result": { "first_name": "Jane", "last_name": "Doe", "domain": "acmecorp.com", "status": "found", "result": { "email": "jane.doe@acmecorp.com", "pattern": "first.last", "tier": 1, "smtp_code": 250, "valid": true }, "is_catch_all": false, "provider": "google", "attempts": 3, "duration_ms": 2140 } }, { "job_id": "b91d2c40-8f1e-47f5-a6c3-2d7e9c0f4b22", "status": "completed", "kind": "find", "result": { "first_name": "John", "last_name": "Smith", "domain": "techstartup.io", "status": "catch_all", "result": null, "is_catch_all": true, "provider": null, "attempts": 5, "duration_ms": 4890 } } ] } ``` ## Response Fields Each job entry carries `job_id`, its lifecycle `status` (`queued`, `processing`, `completed`, `failed`), and `kind`, either `find` or `verify`, identifying which result shape the job produces. `kind` is present on newer API versions; treat it as optional. Per-job `result` object: | Field | Type | Description | |-------|------|-------------| | status | String | Enumeration outcome: `found`, `not_found`, `catch_all`, `catch_all_pattern`, `undeliverable`, `inconclusive`, `ambiguous`, `error`, `timeout`, `greylisted`, `rate_limited` | | result | Object | Matched address details when found: `email`, `pattern`, `tier`, `smtp_code`, `valid`. `null` otherwise | | is_catch_all | Boolean | Whether the domain accepts mail for any address, making per-address verification inconclusive | | provider | String | Detected mail provider for the domain, when known | | attempts | Integer | SMTP verification attempts made | | duration_ms | Integer | Processing time for the job | Only jobs whose `status` is `found` are billed. A `catch_all` or `catch_all_pattern` outcome means the address could not be proven; any pattern returned there is a guess and is not billed. An `ambiguous` outcome means more than one address pattern is live and none could be singled out; no address is returned and it is not billed. The Python SDK exposes this as `client.email.find(...)` and `client.email.find_batch(...)` - see the [Python SDK reference](https://docs.discolike.com/sdk/reference/#email). ## Extract API Extract text content from any web page URL. ``` GET https://api.discolike.com/v1/extract ``` The request is priced before it runs; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | url | URL of the web page to extract. | | domain | Bare domain to extract, an alias for `url=https://{domain}`. Hits the cached page when available. Provide one of `url` or `domain`. | ## Response Fields | Field | Type | Description | |-------|------|-------------| | language | String | Detected language (ISO-639-2 code) | | text | String | Extracted text content | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/extract?url=https://acmecorp.com/" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "language": "en", "text": "Acme Corp - Enterprise Solutions. We provide cutting-edge software solutions for businesses of all sizes..." } ``` ## Growth API Quarterly growth metrics derived from a company's digital footprint, tracking [Score](https://docs.discolike.com/api/endpoints/score/) and subdomain changes over time. Use with [BizData](https://docs.discolike.com/api/endpoints/bizdata/) for full firmographic context. ``` GET https://api.discolike.com/v1/growth ``` The request is priced before it runs; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Domain to analyze | ## Response Fields | Field | Type | Description | |-------|------|-------------| | domain | String | Normalized domain | | score_YYYYQX | Integer | Digital footprint score for each quarter | | score_growth_3m | Float | Score growth rate (last 3 months) | | subdomains_YYYYQX | Integer | Subdomain count for each quarter | | subdomain_growth_3m | Float | Subdomain growth rate (last 3 months) | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/growth?domain=acmecorp.com" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "domain": "acmecorp.com", "score_2024Q1": 58, "score_2024Q2": 66, "score_2024Q3": 87, "score_growth_3m": 11.2, "subdomains_2024Q1": 5, "subdomains_2024Q2": 4, "subdomains_2024Q3": 3, "subdomain_growth_3m": -1.9 } ``` ## LLM Providers API Manage LLM provider integrations used by [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/). Connect OpenAI, Anthropic, or any OpenAI-compatible endpoint (self-hosted models via LiteLLM, Ollama, etc.). See the [Self-Hosting guide](https://docs.discolike.com/guides/self-hosting/) for setting up your own model. ``` GET https://api.discolike.com/v1/llm-providers/config POST https://api.discolike.com/v1/llm-providers/config GET https://api.discolike.com/v1/llm-providers/config/{integration_id} PUT https://api.discolike.com/v1/llm-providers/config/{integration_id} DELETE https://api.discolike.com/v1/llm-providers/config/{integration_id} POST https://api.discolike.com/v1/llm-providers/test-connection POST https://api.discolike.com/v1/llm-providers/config/{integration_id}/set-default ``` ## Integration Fields | Field | Type | Description | |-------|------|-------------| | integration_id | UUID | Auto-generated unique identifier | | integration_name | String | Human-readable name for this integration | | provider | String | `openai`, `anthropic`, `custom` (for self-hosted / BYOM), or `typesafe` (for [typed columns](https://docs.discolike.com/api/endpoints/discogen/#typed-columns)) | | api_key | String | Provider API key (masked as `*****` in responses) | | model_name | String | Model identifier (e.g., `gpt-4o`, `claude-sonnet-4-6-20250514`, `openai/llama4:scout`), with [provider settings](#regions-and-accounts) after `#` where the provider needs them | | base_url | String | Required for `custom` provider: your endpoint URL | | supports_web_search | Boolean | Whether the model supports built-in web search | | is_default | Boolean | Whether this is the default integration | | model_deprecated | Boolean | Whether the model is no longer available | ### Validation - `integration_name`, `provider`, and `model_name` must be non-blank. - `api_key` is required on create and test-connection, optional on update. Changing `provider`, `model_name`, or `base_url` on update requires a new `api_key`. - `provider: custom` requires `base_url`. - `base_url` must be an `http(s)` URL with a valid public hostname, no whitespace, quotes, or angle brackets. - `provider: typesafe` cannot be set as the organization default (`POST .../set-default` 400s with `A TypeSafe integration answers typed columns only and cannot be the organization default model`). ## List Integrations ```shell curl "https://api.discolike.com/v1/llm-providers/config" \ -H "x-discolike-key: API_KEY" ``` ```json { "providers": [ { "integration_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "integration_name": "GPT-4o", "provider": "openai", "api_key": "*****", "model_name": "gpt-4o", "base_url": null, "supports_web_search": true, "is_default": true, "model_deprecated": false } ], "mtime": "2026-03-28T12:00:00Z" } ``` ## Create Integration ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/config" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "Claude Sonnet", "provider": "anthropic", "api_key": "sk-ant-...", "model_name": "claude-sonnet-4-6-20250514" }' ``` ```json { "message": "LLM integration 'Claude Sonnet' created successfully", "integration_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012" } ``` For custom/self-hosted providers, include `base_url`: ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/config" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "Self-hosted Llama", "provider": "custom", "api_key": "your-litellm-key", "model_name": "openai/llama4:scout", "base_url": "https://your-tunnel.trycloudflare.com" }' ``` ### Regions and Accounts Some providers need a setting besides the key. Append it to `model_name` after `#`, as URL query parameters: | Provider | Settings | Example `model_name` | |----------|----------|----------------------| | `bedrock`, `bedrock_mantle` | `region` (optional, the provider's default when omitted) | `us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1` | | `snowflake` | `account` (required), your Snowflake account identifier, e.g. `orgname-accountname`; `api_key` is a programmatic access token | `snowflake/claude-4-sonnet#account=myorg-myaccount` | | `watsonx` | `region` and `project` (both required), the project ID being a UUID | `watsonx/ibm/granite-3-8b-instruct#region=eu-de&project=` | The accepted regions are listed under `provider_regions` in [List Models](https://docs.discolike.com/api/endpoints/discogen/#list-models). A setting the provider does not take, a missing required one or an unknown region answers `400`. ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/config" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "Bedrock us-east-1", "provider": "bedrock", "api_key": "your-bedrock-api-key", "model_name": "us.anthropic.claude-sonnet-4-5-20250929-v1:0#region=us-east-1" }' ``` ### TypeSafe Integrations A `typesafe` integration answers [typed columns](https://docs.discolike.com/api/endpoints/discogen/#typed-columns) — the yes/no, fixed-set, and scale judgments DiscoGen can detect in a query — instead of generating text, so it needs no `base_url` and its `model_name` is a TypeSafe model such as `jev-latest`: ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/config" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "TypeSafe", "provider": "typesafe", "api_key": "your-typesafe-key", "model_name": "jev-latest" }' ``` Any DiscoGen run that requests `typed_columns: true` uses your TypeSafe integration regardless of which `integration_id` the rest of the query runs on. If your organization has more than one, the oldest is used. Pick a TypeSafe integration as `integration_id` explicitly to run typed-only instead, with no LLM provider call at all. ## Update Integration Omit `api_key` to keep the existing key. ```shell curl -X PUT "https://api.discolike.com/v1/llm-providers/config/{integration_id}" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "GPT-4o (Updated)", "provider": "openai", "model_name": "gpt-4o" }' ``` ## Delete Integration Requires admin role. ```shell curl -X DELETE "https://api.discolike.com/v1/llm-providers/config/{integration_id}" \ -H "x-discolike-key: API_KEY" ``` ## Test Connection Validate a provider configuration before saving. ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/test-connection" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "openai", "api_key": "sk-...", "model_name": "gpt-4o" }' ``` ```json { "status": "success", "message": "Connection to openai successful" } ``` ## Set Default Sets an integration as the organization default. Requires admin role. ```shell curl -X POST "https://api.discolike.com/v1/llm-providers/config/{integration_id}/set-default" \ -H "x-discolike-key: API_KEY" ``` ## Match API Match a company name to domain profiles. Returns ranked candidates with confidence scores. For bulk matching, see [Match Bulk](https://docs.discolike.com/api/endpoints/bulk-match/). To enrich matched domains with firmographic data, use [BizData](https://docs.discolike.com/api/endpoints/bizdata/) or [Append](https://docs.discolike.com/api/endpoints/append/). ``` GET https://api.discolike.com/v1/match ``` The request is priced before it runs at its request fee plus the most matches it can return; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | name | Company name to match (required). Must be non-empty after whitespace and punctuation are stripped. Pass the name alone when you can: if a name of three or more words finds no strong match (for example `Alasco construction finance software Munich`), the company name and any country it mentions are read out of it and matched instead, which adds a couple of seconds. [Match Bulk](https://docs.discolike.com/api/endpoints/bulk-match/) does the same per row. Not applied with `strict` | | country | ISO-3166-1 alpha-2 country code | | state | State or region. Accepts an ISO 3166-2 code (e.g. `CA`, `BY`) or a full name (e.g. `California`, `Bayern`) for any country that has subdivisions. Values that cannot be resolved are ignored | | city | City name | | zip_code | Zip code | | phone | Phone in E.164 or local format | | min_match_confidence | Minimum match_confidence (inclusive) a match must have, 50-100 (default 50) | | strict | Exact-name matching (boolean, default false). The name is searched as a quoted phrase with no abbreviation expansion, and only candidates whose `name` or an alternate name equals it after normalization are kept. Filter relaxation is limited to clearing `state` then `country` | | local_mode | Keep `state` and `country` during filter relaxation (boolean, default false). Other filters are still dropped when a pass returns nothing, and candidates missing the location are excluded instead of treated as unknown | Matches with `match_confidence` below 50 are never returned; `min_match_confidence` raises this floor. ## Response Fields | Field | Type | Description | |-------|------|-------------| | query | Object | Query parameters used | | matches | Array | [BizData](https://docs.discolike.com/datasets/bizdata/) profiles with match_confidence | Each match includes `match_confidence` (0-100) indicating how well it matches the query. ## Errors | Status | Meaning | |--------|---------| | 204 | No company matched the query | | 503 | The search backend could not be reached after retries — retry the request; this is not a no-match | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/match?name=acme+software&country=US" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "query": { "name": "acme software", "country": "US" }, "matches": [ { "domain": "acmesoftware.com", "name": "Acme Software Inc", "match_confidence": 100, "score": 250, "address": { "state": "California", "country": "US" } } ] } ## Bulk Match API Match a CSV of company names to domain profiles. Async processing, returns one best match per row. ``` POST https://api.discolike.com/v1/bulkmatch ``` ## Parameters Upload CSV with `Content-Type: multipart/form-data`. | Parameter | Description | |-----------|-------------| | name_column | Column with company names (required) | | country_column | Column with country codes | | state_column | Column with states, as ISO 3166-2 codes or full names for any country that has subdivisions. Values that cannot be resolved are ignored | | city_column | Column with city names | | zip_code_column | Column with zip codes | | phone_column | Column with phone numbers | | min_match_confidence | Minimum match_confidence (inclusive) a match must have, 50-100 (default 50) | | strict | Exact-name matching (boolean, default false), applied to every row. Same semantics as [Match](https://docs.discolike.com/api/endpoints/match/) | | local_mode | Keep `state` and `country` during filter relaxation (boolean, default false). Same semantics as [Match](https://docs.discolike.com/api/endpoints/match/) | Matches with `match_confidence` below 50 are never returned; `min_match_confidence` raises this floor. Rows are billed when the results are ready, one record per row that was searched. A cancelled task returns, and bills, only the rows it searched before the cancel. An upload whose rows would cost more than the account's remaining balance is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance) that names the largest row count that fits. ## Response Fields | Field | Type | Description | |-------|------|-------------| | status | String | `in_progress`, `completed`, `cancelled`, or `failed` | | progress | Integer | Progress percentage | | results | Array | [BizData](https://docs.discolike.com/datasets/bizdata/) profiles with `input:` prefix | | match_confidence | Integer | Match confidence 0-100 | | match_error | String | Present only when the row could not be searched: `search_failed`. The row is not a no-match and is not billed — resubmit it. | ## Examples ### Request ```shell curl --form file='@data.csv' \ "https://api.discolike.com/v1/bulkmatch?name_column=name" \ -H "x-discolike-key: API_KEY" ``` ## Initial Response ```json { "task_id": "e362cb75-35e2-4006-a239-3d87a7472872" } ``` An account can have three DiscoGen, bulk match, contact bulk match and segment tasks running at once. Starting another answers `429` with `Retry-After: 60` until one finishes or is cancelled; see [limits on work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress). ## Check Status ```shell curl "https://api.discolike.com/v1/bulkmatch/status/{task_id}" \ -H "x-discolike-key: API_KEY" ``` ```json { "status": "in_progress", "progress": 30 } ``` ## Final Response ```json { "status": "completed", "progress": 100, "results": [ { "input:name": "acme software", "domain": "acmesoftware.com", "match_confidence": 100, "name": "Acme Software Inc" }, { "input:name": "globex", "domain": null, "match_error": "search_failed" } ] } ## Managed Prospecting API Managed prospecting interprets company and persona requirements, then runs company matching or discovery, ICP validation, indexed contact search, ContaGen gap filling, and email finding. Optional segmentation groups accepted companies. A run can also deliver companies only, with no contact work (see [Companies-Only Runs](#companies-only-runs)). REST runs wait for your approval before research starts; chat messages let you steer the run or answer a clarification question. ## Start a Run `POST /prospecting/runs` Provide normal API authentication, a JSON body, and an `Idempotency-Key` header (1–128 characters). Reuse the key and identical request when retrying. A changed request, including changing an omitted setting into an explicit setting, returns `409` for the same key. The response is `202` with a stable `run_id` and `drafting` status; planning continues after you disconnect. | Field | Type | Default | Description | | --- | --- | --- | --- | | `brief` | string | Required | Company and persona requirements, 10–4,000 characters. | | `domains` | string[] | `[]` | Explicit target companies, up to 1,000; these are not lookalike examples. | | `company_names` | string[] | `[]` | Up to 100 company names to match. | | `customer_domains` | string[] | `[]` | Up to 1,000 of your customers' domains. The run groups them into segments and finds lookalikes of each; they are never returned as results. Cannot be combined with `domains` or `company_names` (422). | | `exclude_domains` | string[] | `[]` | Up to 1,000 domains to suppress. | | `target_companies` | integer | `1000` | Companies with accepted contact coverage (matching companies on a companies-only run); 1–10,000. If omitted, the brief can supply the count. | | `contacts_per_company` | integer | `3` | Accepted contacts per company; 1–10. If omitted, the brief can supply the count. | | `max_candidates` | integer | `0` | Automatic when `0`; otherwise 1–100,000, at least the target and supplied company count. | | `max_actions` | integer | `0` | Automatic when `0`; otherwise 1–10,000. Polling an existing child does not create another action. | | `validation_integration_id` | string or null | `null` | Your LLM integration, organization default, or explicit `native-icp` (DiscoLike VibeCheck). Left open, it runs on your default or only AI integration; with none, on DiscoLike VibeCheck; with several and none marked default, the plan leaves it for you to pick. | | `contact_integration_id` | string or null | `null` | Your LLM integration, or explicit `native` (DiscoLike Groove). Left open with a search provider available, research runs on DiscoLike Groove. | | `search_provider_id` | string or null | `null` | Your search integration, organization default, or only search provider (with several and no default, the plan leaves it for you to pick). `"none"`, or having no search provider, finds indexed contacts only with no web research. | | `segment` | boolean | `false` | Segment qualified companies when at least ten are available. | | `deliverable` | string | `"leads"` | `"leads"` finds people to contact at the companies; `"accounts"` returns the checked companies only, with no contact work (see [Companies-Only Runs](#companies-only-runs)). | | `goal` | string | `"leads"` | `"leads"` keeps adding rounds of companies until the contact target is met; `"companies"` takes `target_companies` matching companies and works only those, finishing with `stop_reason: "companies_worked"` even if some of them have no reachable people. | | `checkpoints` | string | `"auto"` | `"ask"` pauses the run in chat at each checkpoint (see [Checkpoints](#checkpoints)); `"auto"` never pauses. Not part of the OpenAPI schema; send it explicitly in the request body. | `checkpoints` is omitted from the schema on purpose, so set it explicitly rather than relying on a generated client to surface it. The REST default is `"auto"`. Omit quantity fields to infer them from the brief, with 1,000 companies and 1 contact per company as fallbacks. Explicit values, including explicit defaults, override the text. Employee counts and revenue remain targeting criteria. Ambiguous quantities or counts outside the limits produce a clarification question. Automatic candidate limits use the largest of `target × 8`, 50, and `len(domains) + len(company_names)`. For example, a target of 25 with 1,000 supplied domains gets a candidate limit of 1,000. Automatic action limits use `ceil(target/10) × 6 + 12` with a minimum of 24. An automatic candidate limit can grow once during the run: after enough companies have been researched, the run raises it from its own measured fit rate and contact yield, within a per-target ceiling and its action budget. A `max_candidates` you set is never raised. Explicit caps remain authoritative. ```bash curl -X POST https://api.discolike.com/v1/prospecting/runs \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: logistics-september-01' \ -d '{"brief":"Find 100 US logistics companies and 3 operations directors at each"}' ``` An active paid account with quota and tenant-owned integrations is required. Explicit native engines must be available on the deployment. Intent interpretation, wizards, segmentation, and prompt preparation use platform credentials. Agent coordination and independent contact qualification use your contact LLM integration; native contacts use your validation LLM integration or organization default. A run whose company checks use DiscoLike VibeCheck (`native-icp`) needs no generative AI integration when its contacts are researched with DiscoLike Groove (`native`), come only from the contact index because no search provider is connected, or are not requested (`deliverable: accounts`): it takes the next permitted stage in a fixed order, and keeps contacts by the title research or the contact index found without checking them against the persona (`role_fit` is `accepted` and `role_reason` says so). Existing processing and provider charges apply. Missing keys and provider failures do not fall back to platform credentials. Work limits (`max_candidates`, `max_actions`) are **not provider-dollar caps**; to cap what a run spends on your own AI and search provider keys, set `max_provider_spend_usd` when you [change the plan](#change-a-plan). A run never spends past your balance: before each billed step it is priced against what is left, and a step the balance cannot cover pauses the run in `needs_input` with `stop_reason: "account_unavailable"` instead of running it. Results found so far are kept, and the run can be resumed after a top-up or upgrade. Starting a run can return `403` when your organization can't start another prospecting run right now; the message says when you can retry. Repeating a request that was already accepted, with the same `Idempotency-Key`, is not affected. ## Review and Approve `POST /prospecting/runs/{run_id}/approve` Poll the run until it reaches `proposed`, then review its `plan` and the `plan` chat message. Send the exact version you reviewed: ```json {"plan_version": 1} ``` `plan_version` is required and must be at least 1. `checkpoints` is optional (`"ask"` or `"auto"`); omit or send `null` to keep the value from the brief. Success returns `202` with the run queued. Repeating approval of the already approved version is safe. A stale version or a plan that is not ready returns `409`; fetch and review the current plan before retrying. When your organization has several AI or search providers and none is set as the default, the plan leaves that engine's `selected` as `null` with `pick_required: true` and approval returns `409` until you pick one with [Change the Plan](#change-a-plan); with a default, or only one provider, the plan uses it. An `Idempotency-Key` header is accepted but not required for approval. One queued or running run per actor is allowed within an organization. `seed_segments` is optional: the ids from the plan card's `seed_segments` to expand. Omit it or send `null` to keep the run's current selection (the plan card's default, or whatever a chat edit set); an empty list or an id the plan does not list returns `422`. `segment` is optional too: `true` or `false` overrides the brief's `segment` (group the results into segments at the end); omit it to keep the brief's value. The `plan` message's `data` also carries `checkpoints` (the mode this run will approve with), `market_size` (an upper bound on companies matching the planned queries, capped at 10,000; `null` when unknown or when the brief supplies its own `domains`/`company_names`), and `all_matching_target` (offered only when `market_size` exceeds `target_companies`, capped at the same maximum: the count you'd get by raising the target to match the whole market). For a run started with `customer_domains`, the `plan` message's `data` also carries `seed_segments` (each with `id`, `name`, `customers`, three `examples` and `selected`) and `segment`. `market_size` then sums only the selected segments' planned filter queries, since lookalike searches have no free count. ## Change a Plan `PATCH /prospecting/runs/{run_id}/plan` Change the engines, size, deliverable, or spending limits of a plan that is waiting for approval: ```json {"plan_version": 1, "search_provider_id": "none", "max_spend_usd": 25, "max_provider_spend_usd": 10} ``` | Field | Type | Description | | --- | --- | --- | | `plan_version` | integer | Required, at least 1. The plan version you reviewed. | | `contact_integration_id` | string | Contact research engine. Use an option `id` from the plan message's `data.contact_engine.options`. | | `validation_integration_id` | string | Company check engine. Use an option `id` from `data.company_check_engine.options`. | | `search_provider_id` | string | Search provider. Use an option `id` from `data.search_provider.options`; `"none"` skips web research and finds indexed contacts only. | | `target_companies` | integer | How many qualified companies the run aims for, 1–10,000. Limits you left automatic are re-derived around it, as a chat edit would. | | `contacts_per_company` | integer | Most contacts to find at each qualified company, 1–10. | | `deliverable` | string | `"leads"` (people to contact) or `"accounts"` (the checked companies only). Switching back to `"leads"` brings back a persona to search for. | | `goal` | string | `"leads"` (keep adding rounds until the contact target is met) or `"companies"` (take `target_companies` matching companies and work only those). | | `max_spend_usd` | number | Most the run may spend on DiscoLike credits: records and per-request fees, in USD at your plan's rates, at least 0. `0` removes the limit. A plan with no per-record price returns `422`. | | `max_provider_spend_usd` | number | Most the run may spend on your own AI and search provider keys, in USD, at least 0. Once reached, the run starts no new work; batches already running finish, so the total can end slightly above it. `0` removes the limit. | Every field except `plan_version` is optional; an omitted field keeps its current value, and a choice made here is never replaced by a later default. Engines you have not picked are re-chosen around your picks (picking a search provider brings research back to DiscoLike Groove). Unknown fields return `422`. The plan message's `data` lists each engine's `selected` id and its `options` (`id` and `label`), plus `target_companies`, `contacts_per_company`, `deliverable`, `goal`, `max_spend_usd`, `max_provider_spend_usd`, `estimate.records` (the records the run is expected to bill: companies checked, indexed contacts and found emails), `estimate.billed_calls` (billed company searches and name matches, company checks, contact searches, research jobs and segmentation), `estimate.records_cost_usd` (records and per-request fees at your plan's rates), `estimate.llm_cost_usd` (estimated spend on your AI and search providers, DiscoLike Groove's web searches included), `estimate.ai_cost_usd` and `estimate.search_cost_usd` (the AI and search providers' parts of `llm_cost_usd`; `null` when the estimate cannot split it), `estimate.search_billed_separately` (true only on plans made before 2026-10-02, whose `llm_cost_usd` left out those searches), `account_spend_left_usd` (what the account can still spend this billing cycle) and, on a [wave run](#waves), `email_lookup_note` (says approving also starts email lookups for contacts already in DiscoLike's database; absent otherwise). The plan is re-estimated at a new `plan_version`, and the latest `plan` message is rewritten in place with the same `seq` rather than posted again, so a `messages_after` cursor past it will not return it. Read the updated plan from this response's `messages` (or poll with `messages_after=0`), then approve the new version. Success returns the run, as `GET /prospecting/runs/{run_id}` does. A value that is not one of the plan's options returns `422`. A run that is not waiting for approval, or a stale `plan_version`, returns `409`. A missing run or another organization's run returns `404`. A request that changes nothing returns the run unchanged, at the same `plan_version`. With no contact engine chosen, contact research runs on DiscoLike Groove when a search provider exists; Groove adds no AI provider cost; its web searches bill your own search provider, about two queries per researched company at that provider's per-query price, and never DiscoLike credits. With no search provider, the plan says contacts come from DiscoLike's contact index only. ### Two spending limits A run can carry two independent limits. `max_spend_usd` caps DiscoLike credits (records and per-request fees at your plan's rates); reaching it stops the run with `stop_reason: "credit_limit"`. `max_provider_spend_usd` caps what the run spends on your own AI and search provider keys; reaching it stops the run with `stop_reason: "provider_limit"`. Results found before either stop are kept. The run response's `provider_cost_usd` reports the provider spend so far, as the providers report it (a custom AI endpoint reports no price), and `provider_ai_cost_usd` and `provider_search_cost_usd` split it between your AI and search providers. The closing message names each part ("Your AI provider key was charged about $X and your search provider about $Y", leaving out a part under a cent); runs started before the split was recorded report only the total. ## Companies-Only Runs Start with `deliverable: "accounts"` (or set it on the plan) when you need the companies and not the people at them. A companies-only run searches for companies and checks each against your ICP, but does no contact search, research or email finding, so it bills no contact or email records and needs no persona. It completes once it has `target_companies` fit companies (`stop_reason: "target_reached"`), or earlier if the searches run dry or a limit is reached. Its results list holds one row per fit company and no contacts (see [Saved Results](#saved-results)). A brief can ask for this in plain words ("just the companies"); when it is unclear whether you want people or companies, the [intake card](#intake) asks. ## Phased Runs New runs and every companies-only run work in phases: they search for and check every company first, then find people at the companies that fit. The run response's `pipeline_phase` says where a phased run is: `"companies"` while it searches and checks companies, `"people"` while it finds people at them, and `null` for older runs that interleave both. A companies-only run stays in `"companies"`. When the people phase ends short of the contact target because too few of the companies had reachable people, a run with `goal: "leads"` proposes another round of companies sized from the yield so far. A round that fits within the run's limits starts on its own, with a text message saying how many companies it adds and what it may cost. A round that needs more room than the spending limits allow pauses at the [`top_up` checkpoint](#checkpoints) instead, even in `auto` mode; a round with no room at all stops the run at that limit. With `goal: "companies"`, the run never adds rounds: it works the companies it took and finishes with `stop_reason: "companies_worked"`. ### Waves Lead runs are moving to waves (being rolled out; runs planned before it reaches your account, and every companies-only run, keep the behavior above). A wave run sizes each wave from the target: enough companies to check that, at the expected fit rate, the share of fit companies with a contact matching your persona, and the email find rate, it should reach the target with a 25% margin. The rates come from history for the first wave and from the run's own results after that. The run checks every company in the wave, with no email lookups while it does, then finds and qualifies contacts at all the fit ones. While it researches contacts on the web, the run already looks up emails for persona-matched contacts that came from DiscoLike's database, in `ask` and `auto` mode alike: approving the plan is your consent for those, and the plan message's `email_lookup_note` says so. A run started over MCP approves its plan automatically, so it looks up no email before the review. It sends one batch at a time, once 50 are waiting or the database search is done, within the lookups the emails the target still needs would take. Contacts found by web research wait for the review. If these early emails reach the target, the run completes without a review. A run whose plan message had no `email_lookup_note` (planned before early lookups reached your account) looks up no email before the review. When the wave's research settles, the run stops before looking up any other email, at the [`contacts_review` checkpoint](#checkpoints), in `ask` and `auto` mode alike. The question says how many companies were checked, how many fit, and how many contacts matched the persona at how many companies, what the early lookups found and how many are still running, plus what finding the remaining contacts' emails would take: how many lookups (every matched contact not already being looked up, capped at the lookups the emails the target still needs would take at the find rate plus a 15% margin, less those still running, raised in a wave after the first to 50 lookups across the wave, early ones included, and capped at what the credit limit leaves), about how long (email lookups are shared and run at about 70 a minute), how many emails to expect at the find rate, and the records of your plan quota reserved until each lookup settles, with the amount in USD when the run has a spending limit (only found emails stay billed). If a limit cut the research short, it says which. Once the run reaches its target, lookups that have not started yet are cancelled and their reserved records released, so they are not billed. - **Find emails** looks up emails for that wave only, up to the number of lookups the question showed. Because it is consent to spend, it counts only when sent with `question_seq` set to the review question's `seq` (see [Send a Message](#send-a-message)); a "Find emails" without it, typed or from an older client, approves nothing, and the agent posts the review again for you to confirm. - **Find more companies first** checks another wave before looking up the remaining contacts' emails and asks again after it, with the contacts found so far kept. It is left out when there are no more candidates to check or the contact rows are full. A message that changes the criteria at this point counts as this choice, never as consent to look up emails. - **Finish** completes the run (`stop_reason: "user_finished"`) with its companies and contacts and no new lookups. The closing message counts the matched companies and contacts. The [saved list](#saved-results) keeps every contact found, with or without an email; a CRM push sends only contacts with a found email. If the emails still fall short of the target, the run proposes the next wave through the usual round of companies, sized from its own measured rates, and that wave is reviewed again before its emails. Every wave after the first gets at least 50 email lookups, its early lookups and the review's together, as long as that many matched contacts are waiting, and is sized at the run's find rate to find them: a wave costs a full research round however few emails it needs, and lookups not yet started when the target is reached are cancelled, so the extra is not billed. A run whose chat was closed for misuse answers Find emails itself and posts the review's numbers as a text message. MCP runs pause at the review like any other run and are answered with the `answer-prospecting` tool. ## Customer Segments A run started with `customer_domains` first groups your customers into up to eight segments (stage `seed_segment`, platform-funded; five or more customers are needed, otherwise they are searched as one group). Customers the index does not know are left out and listed in the plan's `issues`. If grouping fails twice, all customers are searched as one group and the plan says so. Each segment then gets its own searches: a lookalike of its most representative customers plus the discover wizard's queries for companies like them. The target is split across the selected segments by their share of your customers (each gets at least 10% of it); a segment that runs out of searches hands its remaining share to the others. Every company row carries `seed_segment` (`id` and `name`); the saved companies list has a `Customer segment` column and each company in a contact list carries `customer_segment`. The closing message counts matching companies per segment. At the pilot check of a large run, a segment whose first companies fit poorly stops being searched while the others continue (`checkpoints: "ask"` offers "Skip the weak segments" instead). In chat, paste your customers' domains and say they are customers or examples to find more like ("companies like these", "our customers are"); domains described as the companies to prospect ("contacts at these") become `domains` instead. On a proposed plan, "only the dental ones" or "skip Other" changes the segment selection and updates the plan. **Results segments.** `segment: true` (or the plan card's "Group results into segments") groups the accepted companies into segments once the run finishes; a run needs at least ten. On a finished run, saying "segment these" in chat groups its accepted companies without reopening the run: the segments are written to the company rows (`segment_id`, `segment_description`), the saved lists are rewritten, and a message lists each segment with its size and three example companies. Customer segments (`seed_segment`) and results segments (`segment_*`) are separate fields; neither overwrites the other. ## Send a Message `POST /prospecting/runs/{run_id}/messages` Send a JSON body with `text` (1–4,000 characters), or `intake` to answer an [intake card](#intake), and a required `Idempotency-Key` header (1–128 characters). When `text` is a suggested reply to a question, also send `question_seq`, the `seq` of that question message (optional, at least 1): the reply then answers that question only. If a newer question has been posted since, the reply answers nothing and the agent says the question changed, so a stale screen can never answer a question it did not show. Without `question_seq`, a reply answers the checkpoint that was open when you sent it, except "Find emails" at a `contacts_review`, which needs `question_seq`. Reuse the key when retrying that message; a duplicate returns the stored message. The `202` response is the user message, with `seq`, `created_at`, `role`, `kind`, `content`, and optional `data`. The agent's reply arrives asynchronously through the messages cursor. ```json {"text": "Make it 250 companies and exclude staffing firms"} ``` Answering a suggested reply, here "Find emails" at a `contacts_review` question with `seq` 42: ```json {"text": "Find emails", "question_seq": 42} ``` Messages can change targeting or size, answer a pending question, approve or stop a run, or ask about progress. Running batches finish under the plan version that started them. Messages are accepted in every status; a finished run retains its results and cannot be restarted by an edit. A chat has a per-conversation message limit; past it, a message gets a fixed reply instead of a model call. A chat repeatedly used for something other than prospecting is closed after a few off-topic or rejected messages in a row: later messages get the same fixed reply, and the run response's `chat_closed` field turns `true`. A run with no approved plan, or an approved run paused waiting on a question, is cancelled with `stop_reason: "misuse"` when its chat closes, and its saved results (if any) are kept; an approved run that is queued or running keeps working and its saved lists keep filling regardless of `chat_closed`. Start a new chat to keep prospecting after a chat closes. ## Checkpoints A run can pause at a decision point mid-run and ask in chat (`checkpoints: "ask"`), or decide on its own without pausing (`checkpoints: "auto"`, the REST default); the `top_up` and `contacts_review` rows below say when `auto` asks too. Each checkpoint fires at most once per condition. When a run pauses, it moves to `needs_input` with `stop_reason` set to the checkpoint's name and posts a `question` chat message. The message's `data` carries `reason` (the checkpoint name), `suggested_replies` (exact strings to send back), and `sample` (checked companies with their fit verdict, for the pilot checkpoint only). Answer by sending one of the suggested replies as `text` to `POST /prospecting/runs/{run_id}/messages`; free-text steering also works, but the exact suggested reply is the reliable path. | `stop_reason` | When it fires | Suggested replies | | --- | --- | --- | | `pilot` | The run validated its first batch of candidates (targets of 500 or more companies pilot a first 100). | Good fit: "Run the full list" or "Stop here". Below 80% fit: "Tell me sharper criteria" (keeps the question open and asks you to type the criteria), "Try suggested language" (only when a rewrite is available), "Run as is", or "Stop here". When a re-pilot on sharpened criteria finds no new companies: "Run the full list anyway" or "Stop here". | | `tail_quality` | A validation batch of at least 20 companies from one search settled under 50% fit. | "Skip that search and continue" or "Stop here". | | `short` | Planned searches ran out short of the target, with room left in the action budget; offered once per run. | "Widen the search" or "Finish with `{found}`" (the companies found so far). | | `target_reached` | The target companies and target emails (`target_companies × contacts_per_company`) are both met. | "That's enough" or "Find `{count}` more" (up to the target, capped at 10,000). | | `contacts_review` | A [wave run](#waves) finished checking and researching a wave and has not looked up its researched contacts' emails (contacts from DiscoLike's database may already be looked up). Asked in `auto` mode too. The question carries the wave's counts and the email estimate. | "Find emails" (send it with `question_seq`), "Find more companies first" (only when another wave has room), or "Finish". | | `top_up` | A [phased run](#phased-runs) needs another round of companies to reach its contact target, and that round needs more room than its spending limits leave. Asked in `auto` mode too. The question says how many companies had reachable people, how many more the round needs, what it may cost, and which limit it crosses. | "Raise the limit and run it" (raises the crossed limit by what the round needs), "Run a smaller round (`{companies}` companies)" (only when the limits leave room for part of it), or "Finish with `{found}`". When no limit is crossed (in `ask` mode), "Run another round" replaces the raise reply. The reply's choice names (`continue`, `raise`, `shrink`, `finish`) are accepted too. | In `auto` mode, the run never pauses: a poor pilot (under 80% fit) gets its criteria automatically sharpened once, then continues on the sharpened criteria with a text notice rather than a question. The run only stops with `stop_reason: "pilot_failed"` and the checked sample in the question message's `data.sample` when the re-piloted fit, after that one sharpening, comes in under 20% (the searches are effectively broken); a fit at or above 20% keeps going. If the sharpening itself fails or finds nothing new, the run continues on the original criteria instead of stopping. A drifting search (the `tail_quality` condition) is dropped automatically and the run continues. A run whose search runs dry finishes as `candidates_exhausted`. Reaching the target keeps going, since `target_reached` only ever asks. A `top_up` round that fits the limits runs with a text notice; one that crosses a limit still asks. A `contacts_review` always asks, because it is your consent to spend on the remaining email lookups. Finishing at a checkpoint by choice stops the run with `stop_reason: "user_finished"`; its saved results are kept. The mode can be changed mid-run with [`PATCH /prospecting/runs/{run_id}`](#rename-a-run-or-switch-its-mode); switching to `"auto"` answers an open checkpoint with its continue choice, except `contacts_review`, which stays open until you answer it. MCP-started runs are always `auto`; a `top_up` round on an MCP run is cut to what the limits allow instead of asking. A `contacts_review` pauses an MCP run too: `get-prospecting-run` returns the open `question` and the `answer-prospecting` tool sends the person's answer (see [MCP Tools](https://docs.discolike.com/mcp/tools/)). An MCP client that sees `stop_reason: "pilot_failed"` should show the user the sample and start a new run with a sharper brief. ## Intake When a brief leaves out details the run needs, planning pauses on one structured question instead of a chain of single questions. The run moves to `needs_input` with `stop_reason: "question"` and posts a `question` message with `data.reason` set to `"intake"`. Its `content` is a one-line headline followed by the field labels (for example `A few details so I target the right list: What do these companies do?, Who should I contact?, How many companies`) and `suggested_replies` is `["Use your best guess"]`, so a client that does not know about intake still shows what is asked. The card itself is in `data.fields`, a list of at most five fields, required ones first. | Field | Type | Description | | --- | --- | --- | | `key` | string | One of `deliverable`, `company_activity`, `industry`, `geography`, `company_size`, `persona_roles`, `list_size`. | | `label` | string | The question text. | | `multi` | boolean | `true` when several options can be picked. | | `options` | object[] | 2–9 options, each with a stable `value` and a `label` of up to 60 characters. | | `allow_other` | boolean | `true` when free text can be sent as `other`. | | `default` | string[] | Option `value`s preselected for optional fields. Empty for required fields. | | `required` | boolean | `deliverable` (when asked), `company_activity` and `persona_roles` are required; the rest are optional. | `deliverable` is asked only when the brief does not say whether you want people or companies; it offers `leads` ("People to contact") and `accounts` ("Just the companies"), and answering `accounts` makes the run [companies-only](#companies-only-runs) and drops `persona_roles` from what must be answered. `company_size` and `list_size` always use fixed options. `company_size` offers `emp_1_10`, `emp_11_50`, `emp_51_200`, `emp_201_1000`, `emp_1000_plus`, `rev_under_1m`, `rev_1m_10m`, `rev_10m_plus`, and `any` (default `any`). `list_size` offers `100`, `500`, `1000` (default), and `all`; `all` means the maximum of 10,000 companies. The options of the other fields are drafted from your brief. Explicit settings win. When the request sets `target_companies`, the card never asks `list_size`. When it sets `max_candidates`, `list_size` offers only sizes within it (the field is left out if fewer than two fit), and a chosen size is capped at `max_candidates`. An unclear or out-of-range count in the brief ("between 300 and 800 companies") is asked as `list_size` on the card instead of a separate question. Answer with `intake` on `POST /prospecting/runs/{run_id}/messages`, keyed by field `key`. Each answer has `values` (option values) and an optional `other` (up to 200 characters). `text` is not needed; the stored user message carries a readable summary of your choices in `content` and the answers in `data.intake`. Fields you leave out keep no answer, so send the defaults you want to apply. Option values other than the fixed `company_size` and `list_size` ones come from the card you received; the ones below are illustrative. ```json { "intake": { "company_activity": {"values": ["sell_services"]}, "persona_roles": {"values": ["founder"], "other": "Head of operations"}, "company_size": {"values": ["rev_1m_10m"]}, "list_size": {"values": ["500"]} } } ``` Your choices are added to the brief as labelled clarification lines without another interpretation pass, `list_size` sets `target_companies`, and the run plans again once. An answer is rejected with `422` when it names a question that is not on the card, picks a value that is not an option, sends `other` for a field without `allow_other`, picks several values for a single-choice field, or leaves a required field empty; the message says which field. It returns `409` ("This question was already answered") when the card is closed: the run resumed, a newer question was posted, or an intake answer was already stored. A chat message that leaves the run waiting, such as a question about the card, keeps it open. Sending both `text` and `intake` stores the summary of the choices, not your text. The card is asked at most once per run. If the brief still has a gap after you answer, the run asks one plain question for it, and that question never repeats a detail you already answered. After that the run plans from its best reading of the brief and your answers instead of asking again (senior decision makers when no people are named); you can still change the plan before approving it. A free-text reply to the card works too: it is handled as any other answer to a question, and the card is not asked again. If the card cannot be drafted, the run falls back to a single question for the first missing required detail; that question counts as the intake round. MCP-started runs never ask intake because they approve automatically; a brief too vague to plan gets the single question instead. ## List Recent Runs `GET /prospecting/runs` `limit` defaults to `20` and accepts `1–50`. `before` (a run ID) keys pagination: pass the last run ID from a page to fetch the next one, ordered by `created_at` then `run_id` descending. An unknown or other-organization run ID returns an empty page rather than an error. Returns an array of organization runs, newest first. Each summary includes `run_id`, `status`, `title`, `stop_reason`, `stage`, a shortened `brief`, `target_companies`, `contacts_per_company`, `qualified_companies`, `accepted_contacts`, `created_at`, and `updated_at`. The response has no total count, so a full page (`limit` items) means more may exist; a short page means you've reached the end. ## Progress and Results `GET /prospecting/runs/{run_id}` Poll approximately every five seconds. Company and contact arrays use the same page offset independently; each has its own total count. | Parameter | Default | Description | | --- | --- | --- | | `offset` | `0` | Nonnegative result offset. | | `limit` | `100` | Result page size, 1–500. | | `events_after` | `0` | Return events after this sequence number. | | `messages_after` | `0` | Return messages after this sequence number. | Use `next_event_seq` and `next_message_seq` on subsequent polls. `reply_pending` is true while the agent still owes a reply to a user message. After a "segment these" request on a finished run it also stays true, past the acknowledgement, until the message with the segments or the failure is posted. That can take more than an hour; if grouping stops without posting an outcome, the flag clears on its own after about 90 minutes. Message kinds are `text`, `plan`, `milestone`, `question`, `ack`, and `error`; `data` carries kind-specific details such as plan estimates, suggested replies, or milestone previews. The response includes: - Identity and lifecycle: `run_id`, `status`, `stop_reason`, `stage`, `phase`, `pipeline_phase` (`"companies"`, `"people"`, or `null`; see [Phased Runs](#phased-runs)), `created_at`, `updated_at`, `error`, and `waiting_for_worker`. - Planning: current `brief`, `title`, `target_companies`, `contacts_per_company`, `plan`, `plan_version`, `approved_plan_version`, and `last_decision`. - Progress: `events`, `next_event_seq`, `messages`, `next_message_seq`, `reply_pending`, and `in_flight` (stage, item count, plan version, state, and start time for active batches). - Results: `companies`, `contacts`, `total_companies`, `total_contacts`, `qualified_companies`, `accepted_contacts`, `fit_companies`, `emails_found`, `offset`, `limit`, `saved_query_id`, `saved_query_ids`, and `companies_saved_query_id`. - Work allowance and spend: `actions_used`, `max_actions`, and `provider_cost_usd` (what the run has spent so far on your own AI and search provider keys, in USD, as the providers report it; `null` for runs that recorded no provider cost), with `provider_ai_cost_usd` and `provider_search_cost_usd` (its AI and search providers' parts; `null` when `provider_cost_usd` is, and for runs started before the split was recorded). - Open question: `question` is set while a `needs_input` run waits on an answer that can resume it, and `null` otherwise. It carries `question_seq` (the question message's `seq`, or `null` for a provider, stage or account stop that posted no question message), `reason` (the `stop_reason`), `content`, `replies` (exact reply labels, empty for free text; outage stops offer "Retry" and "Cancel"), and `estimate` (the email estimate on a `contacts_review`, otherwise `null`). It is `null` while a reply is still being read. A `needs_input` run with no `question`, such as `run_timeout`, cannot be resumed. - Chat: `chat_closed` is `true` once the chat has been closed for off-topic use. An approved run keeps working and its saved lists keep filling even when `chat_closed` is `true`. Statuses are `drafting`, `proposed`, `queued`, `running`, `needs_input`, `completed`, `failed`, and `cancelled`. `proposed` needs approval. `needs_input` can carry a question or a provider problem; inspect `question`, `error` and the agent message. Scope or targeting questions use `stop_reason: "question"`; their message `data.reason` can be `out_of_scope`, `company_target`, `persona_target`, `ambiguous_target`, `result_quantity`, or `intake` (see [Intake](#intake)). Answer through the message endpoint. Interpretation attempts consume an action even when they need clarification. An `error` message's `data.reason` can also be `message_limit` (the conversation's per-chat message limit was reached; a fixed reply is returned instead of a model call) or `chat_closed` (the chat was closed for off-topic use). Neither reason implies the run itself failed. Inspect `stop_reason`: completion can mean the target, candidate supply, or an operational limit ended the run. `candidates_exhausted` means only that the search ran dry. `candidate_limit` means the run reached its candidate cap; raise `max_candidates` or ask for fewer companies in a new run. `credit_limit` means the run stopped at its DiscoLike credit limit (`max_spend_usd`), and `provider_limit` at its AI and search provider limit (`max_provider_spend_usd`), with its results kept; start a new run with a higher limit to continue. `companies_worked` means a `goal: "companies"` run worked through the companies it took. `misuse` means the chat was closed for off-topic use; `pilot_failed`, `user_finished`, `top_up` and `contacts_review` come from a [checkpoint](#checkpoints). Potentially billed work interrupted before acknowledgement is marked uncertain instead of blindly repeated, except an interrupted indexed contact search, which is retried because it bills only contacts new to your account. Runs have a 72-hour lifetime, including time waiting for input. Partial results remain available after stopping. A company needs accepted ICP fit. A counted contact needs accepted persona fit and a nonempty email with finder status `found`, `catch_all`, or `catch_all_pattern`. These email statuses remain distinct; they are not delivery guarantees. Indexed and generated contacts undergo independent assessment against profile evidence. Similarity scores and upstream title labels alone do not establish acceptance. Stored contacts are bounded by `target × contacts_per_company × 5`, with a minimum of 500 and maximum of 50,000. Segmentation adds cluster fields to companies; cluster probability is not ICP fit confidence. Email processing finds addresses from a person's name and company domain. It does not expose the app-only raw-address verification API. ## Saved Results Approved runs expose a stable `saved_query_id` for their saved results. The list is grouped by company: every fit company is a parent row with its `domain` and fit columns (`ICP Fit`, `Confidence`, `Reasoning`, and, when present, `Segment` and `Customer segment`), and the contacts found for it are nested in its `contacts`. Companies with a found email come first, then catch-alls, then the rest, and fit companies with no contacts last; within a company, accepted contacts lead. Opted-out contacts are left out and duplicate emails removed. A [companies-only run](#companies-only-runs) has parent rows only, with no contacts. Results refresh at workflow checkpoints and stops; the saved snapshot can lag live progress. A large result is split across several saved queries ("parts") instead of being truncated: each part holds up to 50 MiB, and a company's contacts are never split across two parts. `saved_query_ids` lists every part in order; the first entry always equals `saved_query_id`. Both are empty until the run has saved anything. All parts use action `thin_prospecting` and are tagged `prospecting`, the run's status tag, and (only when the result is split) `part`. When there is more than one part, every part (including part 1) is named "` (part k of N)`"; a single, unsplit result keeps the plain run title. Parts 2..N also carry `query_params.saved_query_part: k`; part 1's params are unchanged. While the run is still going, contacts can move between parts as later checkpoints reproject the result, so treat parts as provisional until the run reaches a terminal status. Find them in the [saved query listing](https://docs.discolike.com/api/endpoints/queries/). Read current company and contact rows through the run endpoint. A companies list is still saved alongside it, for every run including companies-only ones: one row per fit company with the same fit columns, at `companies_saved_query_id`. It stays a single saved query; if it hits its own row cap, it is tagged `partial` instead of silently dropping rows. ## Rename a Run or Switch Its Mode `PATCH /prospecting/runs/{run_id}` ```json { "title": "Logistics operations leaders" } ``` ```json { "checkpoints": "auto" } ``` | Field | Type | Description | | --- | --- | --- | | `title` | string | New chat title. Surrounding whitespace is trimmed, and the trimmed title must be 1 to 80 characters. | | `checkpoints` | string | `"ask"` (Manual) pauses the run at later [checkpoints](#checkpoints); `"auto"` (Auto) never pauses. | Send `title`, `checkpoints`, or both; a body with neither, a blank or too-long title, an unknown mode, or extra fields returns `422`. Returns the run summary, the same shape as each row of [List Recent Runs](#list-recent-runs). A missing, deleted, or other-organization run returns `404`. A title alone works in any status. A title set before the agent plans is kept; the agent only names a chat that has no title yet. Renaming does not rename result lists the run already saved. `checkpoints` switches a run between Manual and Auto while it is still going (drafting, proposed, queued, running, or waiting for input). Switching to `"auto"` while the run is paused at a checkpoint answers that checkpoint with its continue choice and resumes the run, as if you had sent the continue reply; a run paused on any other question keeps waiting. A run that is about to pause at a checkpoint when you switch to `"auto"` keeps going instead. A `contacts_review` is the exception to both: it stays open in `auto` until you answer it. A mode switch is gated like starting a run: it needs a plan with prospecting, an active subscription and a payment method (`403` otherwise) and remaining quota (`402` otherwise). A finished run returns `409`, and so does resuming a paused run while another of your runs is queued or running. When both fields are sent, the mode switch is applied first, and a failed switch leaves the title unchanged. ## Cancel a Run `POST /prospecting/runs/{run_id}/cancel` Stop scheduling new work. Supported children receive cancellation; already submitted work can still finish and incur its normal charges. Cancellation cleanup is bounded. Completed results are retained; active children remain visible in `in_flight` while cleanup continues. Returns the run. ## Delete a Run `DELETE /prospecting/runs/{run_id}` Removes the chat from the organization's run list and hides the run from get and list. An active run is cancelled first. Returns `204 No Content`. Charges already incurred are not refunded. Saved result lists (the queries in `saved_query_ids`) remain available in the [saved query listing](https://docs.discolike.com/api/endpoints/queries/). Later calls for the deleted run return `404`. Starting a new run with the deleted run's `Idempotency-Key` returns `409`; use a new key. ## MCP, SDK, and CLI Access MCP exposes `start-prospecting` (`brief`, `idempotency_key`; the brief takes `customer_domains` for a seeded run), `get-prospecting-run` (`run_id`, `offset=0`, `limit=100`, `events_after=0`), and `cancel-prospecting` (`run_id`). **MCP start automatically approves the run**, so when the brief names no company count, `start-prospecting` defaults `target_companies` to 100 rather than the REST/SDK default of 1000, since approval (and the spend it triggers) happens immediately with no review step. Its page limit remains 100, and its response omits REST chat fields; it does carry `saved_query_ids` (every saved-list part, in order) and `pilot_sample` (the checked companies, populated once `stop_reason` is `pilot_failed`). With several AI or search providers and no default, set `validation_integration_id`, `contact_integration_id` or `search_provider_id` in the brief; otherwise the run stops waiting for input and its error lists the provider ids to choose from. There is no MCP tool to answer a checkpoint; MCP runs always use `checkpoints: "auto"` (see [Checkpoints](#checkpoints)). The approval, message, list, rename, and delete operations described above are REST capabilities. The [Python SDK](https://docs.discolike.com/sdk/reference/#prospecting) exposes sync/async `start`, `get`, `list`, `approve`, `message`, `rename`, `cancel`, `delete`, and `wait`. The [CLI](https://docs.discolike.com/cli/reference/#prospecting) exposes corresponding commands (`status` for `get`) except `rename` and `delete`. Both stop waiting at `proposed` so you can review and approve the plan. ## PublicLink API Find related domains by shared public contact information (social links, phone numbers, emails). ``` GET https://api.discolike.com/v1/publiclink ``` The lookup is priced on the rows it found before they are returned; when the remaining balance cannot cover them, it is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Domain to discover linkage for | | source | Linkage source: `email`, `social`, or `phone` | ## Response Fields | Field | Type | Description | |-------|------|-------------| | domain | String | Normalized query domain | | linked_domain | String | Related domain found | | link_values | Array | Shared contact values establishing the link | | record_date | Date | Date the record was compiled | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/publiclink?domain=acmehotels.com&source=email" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json [ { "domain": "acmehotels.com", "linked_domain": "acmeresorts.com", "link_values": ["privacy@acmehotels.com", "dpo@acmehotels.com"], "record_date": "2024-07-17" }, { "domain": "acmehotels.com", "linked_domain": "acmehotels.co.uk", "link_values": ["privacy@acmehotels.com"], "record_date": "2024-07-17" } ] ## Queries ## List Saved Queries Returns saved queries that have associated domains. Use this to find queries for inclusion/exclusion in discovery. ``` GET /queries/saved ``` ### Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | max_records | integer | 100 | Maximum records to return (1-1000) | | offset | integer | 0 | Records to skip for pagination. Must be 0 or greater | | action | string | none | Filter by action type (e.g. `discover`, `exclusion`) | | tags | array[string] | none | Filter by tags; returns queries matching **any** of the given tags. Repeat the parameter (`tags=a&tags=b`) or pass comma-separated (`tags=a,b`). | Tag filters are normalized the same way stored tags are (lowercased, spaces become hyphens), so `tags=Q2 Outbound` matches a stored `q2-outbound`. ### Response ```json { "results": [ { "query_id": "uuid", "query_name": "My Discovery Query", "action": "discover", "user_name": "John Doe", "mtime": "2026-01-29T12:00:00", "domains": ["company1.com", "company2.com"], "domain_count": 2, "query_params": { ... }, "tags": ["campaign-q1"] } ], "count": 50 } ``` --- ## Save Query Results Save a set of result rows as a reusable saved query. This is the REST equivalent of the `save-mcp-query` MCP tool: it stores a "thin" wrapper (the domains plus any custom columns you send), and the full company data is re-hydrated from your domains when the query is opened in the app. ``` POST /queries/save-results ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | query_name | string | Yes | Name for the saved query (1-255 characters) | | action | string | Yes | Underlying action: `discover`, `segment`, `contacts`, `append`, or `match`. Stored as `thin_<action>` | | data | array[object] | Yes | Result rows. Each row should include a domain (see `domain_column`); any extra columns are preserved | | query_params | object | No | Original query params, kept for reconstructing the query in the UI | | domain_column | string | No | Column holding the domain. Default `domain` | | tags | array[string] | No | Tags to attach (max 20 tags, each 2-50 characters) | ```shell curl -X POST "https://api.discolike.com/v1/queries/save-results" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query_name": "US fintech shortlist", "action": "discover", "data": [{"domain": "stripe.com", "score": 0.92}, {"domain": "plaid.com", "score": 0.88}], "tags": ["fintech", "outbound-q3"] }' ``` ### Response ```json { "query_id": "uuid", "query_name": "US fintech shortlist", "action": "thin_discover", "domain_count": 2, "persona_id_count": 0, "row_count": 2, "tags": ["fintech", "outbound-q3"] } ``` ### Notes - Domain-based actions (`discover`, `segment`, `append`, `match`) require at least one domain across `data`; only `contacts` accepts persona-only rows (via a `persona_id` column). Requests satisfying neither return `400`. - Values in the domain column must look like domains (e.g. `acme.com`); rows with other values make the request return `400` listing the offending values. - `persona_id` values must be integers: fractional numbers, booleans, and strings return `400` rather than being coerced. - Limits: up to 50,000 rows and 50 MB of serialized payload. Larger requests return `413`. To save a larger reusable list of domains or persona IDs, use [Create Exclusion List](#create-exclusion-list). - Rate limit: 10 requests/minute base, scaled by your plan's rate-limit multiplier. - The saved query appears in `GET /queries/saved` and can be used with `inclusion_query_id` / `exclusion_query_id` in discover and contacts queries. --- ## Create Exclusion List Create a new exclusion list from a list of domains or persona IDs. The list can be used for inclusion/exclusion filtering in discovery and contacts queries. ``` POST /queries/exclusion-list ``` ### Request Body **Domain list example:** ```json { "query_name": "My Exclusion List", "domains": ["competitor1.com", "competitor2.com", "competitor3.com"], "tags": ["competitors", "scheduled-removal"] } ``` **Persona ID list example (from contacts search results):** ```shell curl -X POST "https://api.discolike.com/v1/queries/exclusion-list" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{"query_name": "Contacted VPs", "persona_ids": [12345678, 87654321]}' ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | query_name | string | Yes | Name for the exclusion list (1-255 characters) | | domains | array[string] | No | List of domains to save. Provide `domains`, `persona_ids`, or both. | | persona_ids | array[integer] | No | Array of persona IDs to save (from contacts search results). Provide `domains`, `persona_ids`, or both. | | tags | array[string] | No | Tags to apply at creation (max 20 tags, each 2-50 characters). Tags are sanitized: lowercased, spaces become hyphens, invalid characters removed. The response echoes the stored form. | ### Response ```json { "query_id": "uuid", "query_name": "My Exclusion List", "domain_count": 3, "tags": ["competitors", "scheduled-removal"] } ``` ### Notes - Minimum of **20 domains** for domain lists (persona-only lists are exempt). Smaller domain lists return `400`. - Maximum of **250,000 domains** or **500,000 persona IDs** per exclusion list (lists exceeding this limit are automatically truncated). The same caps apply when a list is used for exclusion; an over-cap list returns `400`. - Duplicate domains and persona IDs are automatically removed - At least one of `domains` or `persona_ids` must be provided - Lists created from contacts search results store persona IDs. Lists created from discover or other endpoints store domains. Both types appear in the same saved queries listing and can be used with `exclusion_query_id` or `inclusion_query_id` parameters. - The created query has `action: "exclusion"` - Use the returned `query_id` in the `inclusion_query_id` or `exclusion_query_id` parameters of the discover or contacts endpoints --- ## Update Query Update the name and/or tags of a saved query. You can update just the name, just the tags, or both in a single request. ``` PATCH /queries/{query_id} ``` ### Request Body ```json { "query_name": "New Query Name", "tags": ["campaign-q1", "enterprise"] } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | query_name | string | No | New name for the query | | tags | array[string] | No | New list of tags (replaces existing) | At least one field must be provided. ### Response ```json { "query_id": "uuid", "query_name": "New Query Name", "tags": ["campaign-q1", "enterprise"] } ``` ## Delete Query Delete a saved query (including exclusion lists) by ID. You can only delete queries owned by your account. ``` DELETE /queries/{query_id} ``` ### Response ```json { "message": "Query deleted successfully" } ``` Returns `404` if no query with that ID exists under your client domain. ## Redirects API Find redirect relationships between domains. Uses data from our [Redirect Domains Dataset](https://docs.discolike.com/datasets/redirect-domains/). ``` GET https://api.discolike.com/v1/redirects ``` The lookup is priced on the rows it found before they are returned; when the remaining balance cannot cover them, it is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Query domain | | match | Match direction: `source` or `linked` | ## Response Fields | Field | Type | Description | |-------|------|-------------| | source_domain | String | Normalized source domain | | source_fqdn | String | Full source URL | | linked_domain | String | Normalized destination domain | | linked_fqdn | String | Full destination URL | | record_date | Date | Date the record was compiled | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/redirects?domain=acmeoldsite.com&match=source" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json [ { "source_domain": "acmeoldsite.com", "source_fqdn": "acmeoldsite.com", "linked_domain": "acmecorp.com", "linked_fqdn": "acmecorp.com", "record_date": "2024-07-02" } ] ## Score API Returns a composite score (0–800, higher is better) for any domain, reflecting company size, time in business, and technology adoption. Based on ten years of SSL certificate history. Subdomains are automatically normalized. Use alongside [BizData](https://docs.discolike.com/api/endpoints/bizdata/) for full firmographic profiles and [Growth](https://docs.discolike.com/api/endpoints/growth/) for quarter-over-quarter changes. The request is priced before it runs; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Methodology The score combines four components: a **base score** from historical certificate activity, a **recency multiplier** that rewards sustained activity, a **growth boost** comparing recent vs older activity, and an **expiration penalty** for certificates nearing expiration. ### Score Components | Component | Description | |-----------|-------------| | **Base Score (β)** | Accumulated points from 10 years of certificate history, weighted by market and time | | **Recency Multiplier (ε)** | Ranges 0-1; penalizes domains where all certificates are very recent | | **Growth Boost (φ)** | Positive if certificate activity increased year-over-year, negative if declined. The decline side is floored at half the base score | | **Expiration Penalty (λ)** | Ramps down over the final 14 days of the newest certificate's validity; 0 once no valid certificate remains | ### How It Works **1. Base Score.** For each of the past 10 years, we calculate how many certificates were issued compared to the expected baseline for the market. More certificates = more points, with a cap at 100% of baseline. **2. Recency Check.** If all certificates were issued within the current year, the score is reduced proportionally. A domain with 3+ years of history gets the full score. **3. Growth Adjustment.** We compare certificate activity from the past 360 days against the prior 360 days (720-360 days ago). Growing companies get a boost; declining activity results in a penalty. A domain with no activity in the prior period receives no adjustment either way, and the penalty can never remove more than half the base score — a company that slows down is ranked below its peers, not erased. **4. Expiration Penalty.** The penalty tracks the newest certificate still valid. It is neutral while more than 14 days of validity remain, then ramps down linearly to zero on the expiration date. Once no valid certificate remains, the score is zero regardless of history. ### Calculation Method #### Parameters | Symbol | Name | Notes | | ------ | ---- | ----- | | ΔT | Days since the first certificate was issued in the last 3 years | Certificates with a validity period over three years are ignored | | ε | Recency Penalty multiplier | Decreases score in case all company certificates were created in the current year | | \|Cy\| | Certificates per year | Number of certificates generated in a year | | βcerts | Baseline Certificate Number | Calculated number of certificates expected for a business | | βpt | Baseline points | The number of points to be awarded for a year (distribution of points between years is not uniform) | | τ | Market Coefficient | Establishes a more accurate baseline for each specific market | | Ω360 | Lookback 360 | Number of certificates generated in the past 360 days | | Ω720 | Lookback 720 | Number of certificates generated between 720 and 360 days | | β | Base score | The baseline score for the domain provided over ten years | | θ | Growth points | Points awarded for growth, currently static at 50 | | φ | Growth boost | Year-over-year growth metric | | φmin | Growth decline floor | Largest share of the base score a decline can remove, currently static at 0.5 | | Δτ | Days until the expiration of certificate | Measured against the latest-expiring certificate still valid on the scoring date | | λd | Expiration penalty range | Range is in days (currently static at 14 days) | | λ | Expiration penalty | Ramps from 1 to 0 across the final λd days of certificate validity | #### Recency Penalty Multiplier (ε) $$ \varepsilon = \min\left(\frac{\Delta T}{365},\, 1\right) $$ #### Base Score (β) $$ \beta = \sum_{y=1}^{10}\left(\min\left(\frac{\lvert C_y \rvert}{\beta_{\text{certs}}},\, 1\right)\cdot \beta_{\text{pt}}\cdot \tau \cdot \left(0.4 + 0.5 \cdot \exp\left(-\frac{(x - 6)^2}{8}\right) + 0.05 \cdot (x - 1)\right)\right) $$ #### Growth Sign (θsign) $$ (\Omega_{360} \geq \Omega_{720} \rightarrow \theta_{\text{sign}} = 1) \wedge (\Omega_{360} < \Omega_{720} \rightarrow \theta_{\text{sign}} = -1) $$ #### Growth Boost (φ) $$ \varphi = \begin{cases} 0 & \Omega_{720} = 0 \\[6pt] \max\left(\dfrac{\Omega_{360} - \Omega_{720}}{\Omega_{360} + \Omega_{720}} \cdot \theta,\; -\beta \cdot \varphi_{\min}\right) & \text{otherwise} \end{cases} $$ #### Expiration Penalty (λ) $$ \lambda = \min\left(\frac{\max(\Delta\tau,\, 0)}{\lambda_d},\, 1\right) $$ λ is 0 when the domain has no certificate valid on the scoring date. #### Final Score $$ \text{Score} = \left\lceil \min\left(\max\left((\beta \cdot \varepsilon + \varphi) \cdot \lambda,\, 0\right),\, 800\right) \right\rceil $$ ``` GET https://api.discolike.com/v1/score ``` ## Parameters | Parameter | Description | | --------- | ---------------------- | | domain | Domain to investigate. | ## Response Fields | Field | Format | Description | | ------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | domain | String | Normalized domain name (excluding any subdomain). | | score | Unsigned Int32 | Final calculated score. | | first_event | Date (YYYY-MM-DD) | Date of first registration event. | | confidence | Decimal (1,2) | Confidence score for classification label. | | parameters | Array[ParameterObjects] | Set of calculation parameters as described in the parameters section. | | base_score | Decimal (3,2) | The baseline score for the domain provided over 10 years (β). | | recency_multiplier | Decimal(1,2) | Decreases score in case of all company certificates were created in the current year (ε). | | growth_boost | Decimal(3,2) | Increases score if the number of certificates opened in the last 360 days is greater than those opened 2 years before, decreases it if vice versa. Never below `-base_score / 2` (φ). | | expiration_penalty | Decimal(1,2) | Ramps from 1 to 0 across the final λd days of certificate validity; 0 when no certificate is valid on the scoring date (λ). | | lookback_360 | Unsigned Int32 | Number of certificates generated in the past 360 days (Ω360). | | lookback_720 | Unsigned Int32 | Number of certificates generated between 720 and 360 days (Ω720). | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/score?domain=acmecorp.com" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "domain": "acmecorp.com", "score": 658, "parameters": { "base_score": 700, "recency_multiplier": 1, "growth_boost": -42.78, "lookback_360": 14796, "lookback_720": 190199, "expiration_penalty": 1 }, "first_event": "2010-03-01" } ``` ## Search Providers API Manage search provider integrations used by [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/) for web search enrichment (BYOS, Bring Your Own Search). When web search is enabled in DiscoGen, search queries are run against your configured provider to supplement domain data with live web results. Supported providers include Tavily, Serper, Parallel AI, and Linkup. ``` GET https://api.discolike.com/v1/search-providers POST https://api.discolike.com/v1/search-providers PUT https://api.discolike.com/v1/search-providers/{integration_id} DELETE https://api.discolike.com/v1/search-providers/{integration_id} PUT https://api.discolike.com/v1/search-providers/{integration_id}/default DELETE https://api.discolike.com/v1/search-providers/{integration_id}/default GET https://api.discolike.com/v1/search-providers/models ``` ## Integration Fields | Field | Type | Description | |-------|------|-------------| | integration_id | UUID | Auto-generated unique identifier | | integration_name | String | Human-readable name for this integration | | provider | String | Provider identifier (e.g., `tavily`, `serper`) | | search_model | String | LiteLLM model key (e.g., `tavily/search`, `serper/search`) | | api_key | String | Provider API key (masked as `*****` in responses) | | base_url | String | Custom endpoint URL (for LiteLLM Proxy setups) | | is_default | Boolean | Whether this is the default search provider | | cost_per_query | Float | Cost per search query in USD | ### Validation - `integration_name`, `provider`, and `search_model` must be non-blank. - `api_key` is required on create, optional on update. Changing `provider`, `search_model`, or `base_url` on update requires a new `api_key`. - `base_url`, when set, must be an `http(s)` URL with a valid public hostname, no whitespace, quotes, or angle brackets. ## List Integrations ```shell curl "https://api.discolike.com/v1/search-providers" \ -H "x-discolike-key: API_KEY" ``` ```json { "providers": [ { "integration_id": "c3d4e5f6-a7b8-9012-cdef-345678901234", "integration_name": "Serper", "provider": "serper", "search_model": "serper/search", "api_key": "*****", "base_url": null, "is_default": true, "cost_per_query": 0.001 } ] } ``` ## Create Integration The provider is validated with a test search before saving. ```shell curl -X POST "https://api.discolike.com/v1/search-providers" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "Tavily Search", "provider": "tavily", "search_model": "tavily/search", "api_key": "tvly-..." }' ``` ## Update Integration Omit `api_key` to keep the existing key. Re-validates if provider, model, or base URL changes. ```shell curl -X PUT "https://api.discolike.com/v1/search-providers/{integration_id}" \ -H "x-discolike-key: API_KEY" \ -H "Content-Type: application/json" \ -d '{ "integration_name": "Tavily (Renamed)", "provider": "tavily", "search_model": "tavily/search" }' ``` ## Delete Integration Requires admin role. ```shell curl -X DELETE "https://api.discolike.com/v1/search-providers/{integration_id}" \ -H "x-discolike-key: API_KEY" ``` ## Set Default Sets a search provider as the organization default. Requires admin role. ```shell curl -X PUT "https://api.discolike.com/v1/search-providers/{integration_id}/default" \ -H "x-discolike-key: API_KEY" ``` ## Clear Default Removes the default flag from a search provider, leaving the organization with no default. Only works if the specified integration is currently the default. Requires admin role. ```shell curl -X DELETE "https://api.discolike.com/v1/search-providers/{integration_id}/default" \ -H "x-discolike-key: API_KEY" ``` ## List Available Models Returns all search models supported by the platform, grouped by provider with cost info. ```shell curl "https://api.discolike.com/v1/search-providers/models" \ -H "x-discolike-key: API_KEY" ``` ```json { "models": { "serper": [ { "name": "serper/search", "cost_per_query": 0.001 } ], "tavily": [ { "name": "tavily/search", "cost_per_query": 0.005 } ] } } ``` ## Segment API Automatically group domains into segments based on business similarities. Upload results from [Discover](https://docs.discolike.com/api/endpoints/discover/) or your own domain lists to identify ICP patterns. Each segment includes a label and the domains that belong to it. ``` POST https://api.discolike.com/v1/segment ``` The upload is priced on its domain count before the task is queued; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters Upload a CSV or Excel file with domains (max 25 MB, 10,000 rows; larger files return `400`) using `Content-Type: multipart/form-data`, or skip the file and pass `domains` as a query parameter. | Parameter | Description | |-----------|-------------| | domains | Comma-separated domains to segment, in place of a file upload | | domain_column | Column containing domains (auto-detected if omitted) | | max_segments | Maximum segments to create, 2-100 (auto if omitted) | | query_id | One or more saved-query IDs (from [`GET /queries/saved`](https://docs.discolike.com/api/endpoints/queries/)) whose resolved company domains are unioned into the set being segmented. Accepts a comma-separated string or repeated values. Each saved query contributes up to its domain cap | Either source must resolve to at least 5 domains and at most 10,000; `domains` and `query_id` are unioned before those limits are applied. ## Response Fields Returns [BizData](https://docs.discolike.com/datasets/bizdata/) profiles with additional segment fields: | Field | Type | Description | |-------|------|-------------| | status | String | `in_progress`, `completed`, or `failed` | | progress | Integer | Progress percentage | | results | Array | BizData profiles with segment info | | segment_id | Integer | Assigned segment ID (`-1` = unclustered) | | segment_description | String | AI-generated segment description (`UNCLUSTERED` for segment `-1`) | | probability | Float | Membership confidence from 0 to 1; not a calibrated probability | Only active businesses are segmented; closed or unindexed input domains are omitted from results. ## Examples ### Request ```shell curl --form file='@data.csv' "https://api.discolike.com/v1/segment" \ -H "x-discolike-key: API_KEY" ``` ## Initial Response ```json { "task_id": "e362cb75-35e2-4006-a239-3d87a7472872" } ``` An account can have three DiscoGen, bulk match, contact bulk match and segment tasks running at once. Starting another answers `429` with `Retry-After: 60` until one finishes or is cancelled; see [limits on work in progress](https://docs.discolike.com/api/rate-limits/#limits-on-work-in-progress). ## Check Status ```shell curl "https://api.discolike.com/v1/segment/status/{task_id}" \ -H "x-discolike-key: API_KEY" ``` ```json { "status": "in_progress", "progress": 30 } ``` ## Final Response ```json { "status": "completed", "progress": 100, "results": [ { "domain": "acmesecurity.com", "name": "Acme Security", "score": 450, "segment_id": 0, "segment_description": "Cybersecurity Platforms and Solutions", "probability": 1.0 } ] } ## Signup API Creates a DiscoLike user and organization for a work email without any browser interaction. Built for AI agents acting on behalf of a person: the agent calls this once, the person receives a confirmation email, and they log in at [app.discolike.com](https://app.discolike.com) to pick a plan and issue API keys. Per-agent instructions: [Agent signup guide](https://docs.discolike.com/guides/agent-signup/). ``` POST https://api.discolike.com/v1/public/signup ``` No authentication. Rate limited per IP. ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | email | String | Yes | The person's work email. Free-mail and disposable domains are rejected. Becomes the login and determines the organization domain | | first_name | String | Yes | 1-40 characters after trimming and Unicode NFC normalization; must contain a letter and may not contain angle brackets or control characters | | last_name | String | Yes | 1-40 characters after trimming and Unicode NFC normalization; must contain a letter and may not contain angle brackets or control characters | | agent | String | No | Name of the agent or framework making the call, e.g. `claude-code`. Defaults to the `User-Agent` header | ## Response Fields | Field | Type | Description | |-------|------|-------------| | status | String | `created` | | email | String | Normalized email the account was created for | | org_domain | String | Organization domain derived from the email | | org_status | String | `created` when a new organization was made, `joined` when one already existed for the domain | | next_step | String | Instruction to relay to the account owner | ## What happens next - The person receives a confirmation email from DiscoLike. Google or Microsoft sign-in with the same email also works and skips the confirmation step. - On first login they accept the terms of service and complete their profile. - The account has no plan yet. API keys are issued from the app after choosing a plan; see [Authentication](https://docs.discolike.com/api/access/). - If an organization already exists for the email domain, the person joins it as a member. ## Errors | Status | Meaning | |--------|---------| | 409 | An account for this email already exists. Log in at app.discolike.com | | 422 | Email domain rejected (free-mail, disposable, or not eligible), or `first_name`/`last_name` fails validation: `"<field> must contain a letter and no angle brackets or control characters"` or `"<field> must be between 1 and 40 characters"` — a domain rejection returns `detail` as a plain string, while a name validation failure returns FastAPI's list of error objects, with the message in each `msg` (pydantic prefixes it `Value error, `) | | 429 | Rate limited | | 502 | Account creation failed upstream. Safe to retry later | ## Examples ### Request ```shell curl -X POST "https://api.discolike.com/v1/public/signup" \ -H "Content-Type: application/json" \ -d '{"email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe", "agent": "claude-code"}' ``` ### Response ```json { "status": "created", "email": "jane@acme.com", "org_domain": "acme.com", "org_status": "created", "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." } ``` ### CLI and SDK ```bash discolike signup --email jane@acme.com --first-name Jane --last-name Doe ``` ```python from discolike import signup result = signup(email="jane@acme.com", first_name="Jane", last_name="Doe") print(result.next_step) ``` ## Subsidiaries API Find parent-subsidiary relationships between domains. Uses data from our [Subsidiaries Linkage Dataset](https://docs.discolike.com/datasets/subsidiaries-linkage/). ``` GET https://api.discolike.com/v1/subsidiaries ``` At most 10,000 subsidiaries are returned. The lookup is priced on the rows it found before they are returned; when the remaining balance cannot cover them, it is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Query domain | | match | What to return for the query domain: `parent` returns its subsidiaries, `child` returns its parent company, `recursive` returns the parent plus all siblings/subsidiaries, `source` and `linked` are advanced role-based matches. | The `recursive` option retrieves all child subsidiaries for all parent domains of the query domain. ## Response Fields | Field | Type | Description | |-------|------|-------------| | source_domain | String | Normalized source domain | | source_fqdn | String | Full source URL | | source_score | Integer | Source domain score (1-800) | | linked_domain | String | Normalized linked domain | | linked_fqdn | String | Full linked URL | | linked_score | Integer | Linked domain score (1-800) | | record_date | Date | Date the record was compiled | | parent_domain | String | Domain with highest score | | child_domain | String | Subsidiary domain | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/subsidiaries?domain=acmemail.com&match=child" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json [ { "source_domain": "acmecorp.com", "source_fqdn": "mail.acmecorp.com", "source_score": 855, "linked_domain": "acmemail.com", "linked_fqdn": "acmemail.com", "linked_score": 84, "record_date": "2025-03-10", "parent_domain": "acmecorp.com", "child_domain": "acmemail.com" } ] ## Usage API Returns your API usage for the current month and historical breakdown. ``` GET https://api.discolike.com/v1/usage ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | month_to_date_requests | Integer | Requests this month | | month_to_date_records | Integer | Records returned this month | | month_to_date_spend | Float | Spend this month | | usage_summary | Object | Monthly breakdown by YYYY-MM | ### Monthly Summary Object | Field | Type | Description | |-------|------|-------------| | access_id | String | API key identifier | | description | String | API key description | | requests | Integer | Request count | | total_records | Integer | Records returned | | monthly_spend | Float | Total spend | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/usage" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json { "month_to_date_requests": 92, "month_to_date_records": 24936, "month_to_date_spend": 59.07, "usage_summary": { "2025-01": { "access_id": "API_KEY", "description": "[API] ACME Corp Integration", "requests": 92, "total_records": 24936, "monthly_spend": 59.07 }, "2024-12": { "requests": 403, "total_records": 98624, "monthly_spend": 237.55 } } } ## Validate API Validate whether a list of companies match your Ideal Customer Profile. Provide your ICP description and domains. DiscoLike generates a validation prompt and evaluates each company, returning structured fit/confidence/reasoning results. Built on the [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/) pipeline. Requires an [LLM provider](https://docs.discolike.com/api/endpoints/llm-providers/) integration, unless you run it on DiscoLike's own [native ICP-fit model](#native-icp-fit-model). ``` POST https://api.discolike.com/v1/validate/icp ``` Results are async. Poll `GET /discogen/status/{task_id}` for completion. A validation is priced when it is submitted, at the request fee plus every input domain; one the remaining balance cannot cover is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). A run whose balance runs out partway stops before the next batch and keeps, and bills, the domains it finished. ## ICP Fit Validation ### `POST /validate/icp` Validates domains against an ICP description. Returns a task ID immediately. ### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | icp_text | String | Yes | Your ICP description (e.g., "B2B SaaS companies in HR and payroll with 50-500 employees"). Must be non-empty | | domains | Array | Yes | List of domains to validate (1-10,000) | | context_mode | String | No | What data the LLM sees per domain: `website` (default, profile + homepage), `profile` (firmographics only), `domain` (name only) | | integration_id | String | No | LLM provider integration UUID. Uses your default, or your only integration, if omitted. Pass `native-icp` to score with DiscoLike's own model instead of an LLM (see [Native ICP-Fit Model](#native-icp-fit-model)) | | web_search | Boolean | No | Enable web search enrichment (default: `false`) | | search_provider_id | String | No | Search provider UUID for web search. Uses your default if omitted | ### Response ```json { "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "column_name": ["Fit", "Confidence", "Reasoning"], "status": "in_progress", "total_domains": 3 } ``` ### Result Schema (per domain) When the task completes (poll via `GET /discogen/status/{task_id}`), each domain returns: | Column | Values | Description | |--------|--------|-------------| | Fit | `Yes`, `No` | Whether the company matches the ICP | | Confidence | `high`, `medium`, `low` | Assessment confidence level | | Reasoning | String | 1-2 sentence explanation | A native run returns a different set of columns. Read `column_name` from the task response rather than hardcoding either set. <Aside type="caution"> The `Fit` verdict is title case, `Yes` or `No`, matching the `ICP Fit` column of a native run. Earlier versions of this endpoint returned lowercase `yes` / `no` on LLM runs; update any exact-string comparison, and compare case-insensitively if you support both. </Aside> ### Example ```bash curl -X POST https://api.discolike.com/v1/validate/icp \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "icp_text": "B2B SaaS companies providing HR and payroll solutions with 50-500 employees", "domains": ["gusto.com", "rippling.com", "stripe.com"] }' ``` Then poll for results: ```bash curl https://api.discolike.com/v1/discogen/status/TASK_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` Example completed result: ```json { "task_id": "a1b2c3d4-...", "status": "completed", "results": { "gusto.com": {"Fit": "Yes", "Confidence": "high", "Reasoning": "HR and payroll platform for SMBs, directly matches ICP"}, "rippling.com": {"Fit": "Yes", "Confidence": "high", "Reasoning": "HR, payroll, and IT platform for businesses, strong ICP match"}, "stripe.com": {"Fit": "No", "Confidence": "high", "Reasoning": "Payment processing infrastructure, not HR/payroll"} } } ``` The status response also carries `estimated_cost`, `cost_metadata` and `warnings`, described under [Cost Metadata and Warnings](https://docs.discolike.com/api/endpoints/discogen/#cost-metadata-and-warnings). Validation runs at low search depth with two fixed queries per domain when `web_search` is enabled; for targeted lookups use [DiscoGen](https://docs.discolike.com/api/endpoints/discogen/) with a prompt that names the source. ## Native ICP-Fit Model Pass `integration_id: "native-icp"` to score every domain with DiscoLike's own ICP-fit model, a cross-encoder served on our GPUs, instead of your LLM provider. The run needs no LLM provider integration and makes no calls on your provider key, so it reports no LLM or search spend. The model was trained on LLM validation verdicts, so a score is what an LLM validator would most likely have said about the company, at no LLM cost. It is not more accurate than an LLM. Differences from an LLM run: - No web search is performed. `web_search` and `search_provider_id` are ignored. - `context_mode` is always `website`, the company profile plus homepage text the model was trained on. Passing `profile` or `domain` has no effect. - The columns differ, as below. `Reasoning` carries the confidence band rather than an explanation: the model returns a score, not text. ### Native Result Schema (per domain) | Column | Values | Description | |--------|--------|-------------| | ICP Fit | `Yes`, `No` | Whether the company matches the ICP. `Yes` when `ICP Score` is `0.50` or above | | ICP Score | String | Calibrated probability that the company matches the ICP, `"0.00"` to `"1.00"`, two decimals | | Reasoning | `High confidence`, `Medium confidence`, `Low confidence - ...` | The confidence band, derived from how far `ICP Score` sits from the `0.50` cutoff: high when the score is `0.15` or below or `0.85` or above, medium between `0.15` and `0.35` or between `0.65` and `0.85`, low between `0.35` and `0.65`. This engine scores rather than explains, so the column carries the band instead of a rationale. A low-confidence verdict is close to a coin flip - measured at 0.598 accuracy on held-out data, against 0.943 for high - so treat it as unresolved rather than as a `No` | ### Native Example ```bash curl -X POST https://api.discolike.com/v1/validate/icp \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "icp_text": "B2B SaaS companies providing HR and payroll solutions with 50-500 employees", "domains": ["gusto.com", "stripe.com"], "integration_id": "native-icp" }' ``` ```json { "task_id": "a1b2c3d4-...", "status": "completed", "results": { "gusto.com": {"ICP Fit": "Yes", "ICP Score": "0.91", "Reasoning": "High confidence"}, "stripe.com": {"ICP Fit": "No", "ICP Score": "0.08", "Reasoning": "High confidence"} } } ``` ### Errors | Status | Meaning | |--------|---------| | 400 | The validation prompt generated from your `icp_text` does not list its criteria under `Mandatory:`, `Reject if:` and `Nice-to-have:`. The native model only answers ICP validation prompts; rephrase the ICP text or pick an LLM integration for it | | 503 | Native ICP-fit validation is not available on this server right now. Retry shortly or pass an LLM `integration_id` | ## Vendors API Find vendor relationships for a domain. Uses data from our [Vendor Integration Dataset](https://docs.discolike.com/datasets/vendor-integration/). ``` GET https://api.discolike.com/v1/vendors ``` The lookup is priced on the rows it found before they are returned; when the remaining balance cannot cover them, it is refused with a [`403 INSUFFICIENT_BALANCE`](https://docs.discolike.com/api/errors/#insufficient-balance). ## Parameters | Parameter | Description | |-----------|-------------| | domain | Query domain | | match | Match as `client` or `vendor` | ## Response Fields | Field | Type | Description | |-------|------|-------------| | client_domain | String | Normalized client domain | | client_fqdn | String | Client's full URL | | vendor_domain | String | Normalized vendor domain | | vendor_fqdn | String | Vendor's full URL (white-label) | | record_date | Date | Date the record was compiled | ## Examples ### Request ```shell curl "https://api.discolike.com/v1/vendors?domain=acmemanufacturing.com&match=client" \ -H "x-discolike-key: API_KEY" ``` ### Response ```json [ { "client_domain": "acmemanufacturing.com", "vendor_domain": "cloudprovider.com", "client_fqdn": "analytics.acmemanufacturing.com", "vendor_fqdn": "analytics-12345.us-east-1.cloudprovider.com", "record_date": "2024-06-07" }, { "client_domain": "acmemanufacturing.com", "vendor_domain": "cdnservice.net", "client_fqdn": "assets.acmemanufacturing.com", "vendor_fqdn": "acme-cdn-prod.cdnservice.net", "record_date": "2024-03-01" } ] ## Errors The DiscoLike API uses conventional HTTP response codes to indicate success or failure of requests. | Error Code | Meaning | |------------|---------| | 400 | Bad Request -- The request could not be processed against your data or the input you sent | | 401 | Unauthorized -- Your API key is wrong | | 403 | Forbidden -- You don't have access to this resource, or the request costs more than the remaining balance (see [Insufficient balance](#insufficient-balance)) | | 404 | Not Found -- The specified resource could not be found | | 422 | Unprocessable Entity -- The request failed parameter validation | | 429 | Too Many Requests -- You're making too many requests, or have too much work queued or running (see [Rate Limits](https://docs.discolike.com/api/rate-limits/)) | | 500 | Internal Server Error -- We had a problem with our server | | 503 | Service Unavailable -- We're temporarily offline for maintenance, or a billed request could not be priced (`Unable to price this request, try again`); retry it | ## Insufficient balance Every billed request is priced before it bills anything, at your plan's per-request fee plus its per-1K record rate, as if every record it could return were new to you (records you already received in the last 90 days are still free when the request is billed). A request the remaining balance for the cycle (plan allowance, carryover and top-up credits, less what you have spent) cannot cover is refused with a `403`. Most requests are refused before they search or call an LLM. Four lookups with no size you set, `/vendors`, `/publiclink`, `/subsidiaries` and `/redirects` (and the MCP `public-company-links`, `subsidiaries-and-parent-companies` and `domain-redirects` tools), price the rows they found instead, so they are refused after the lookup but before anything is returned or billed. Accounts without a spending cap are never refused. The `detail` of this `403` is an object, not a string: ```json { "detail": { "code": "INSUFFICIENT_BALANCE", "message": "This request could cost up to $140.70 but the account has $43.75 left this cycle. Lower it to at most 3075 records, or upgrade or top up to continue.", "remaining_usd": 43.75, "max_affordable_records": 3075, "requested_records": 10000, "upgrade_url": "https://app.discolike.com/account/management/plans" } } ``` | Field | Type | Description | |-------|------|-------------| | `code` | string | Always `INSUFFICIENT_BALANCE` | | `message` | string | Human-readable explanation, safe to show to a user; when the request has a size you can lower, it names the largest one that fits and its unit | | `remaining_usd` | number | Balance left in the current cycle | | `max_affordable_records` | integer | The largest size the same request could ask for and still fit (`0` when even the request fee does not fit), in the request's own unit | | `requested_records` | integer | The size the request asked for, in the same unit | | `upgrade_url` | string | Where to upgrade the plan or buy top-up credits | Both counts are in the unit you size the request by, which the `message` names: `max_records` for `/discover` and `GET /contacts` (records), `max_records` or `max_companies` for `POST /contacts/discover` (contacts or companies; a page also bills the companies its contacts come from), the uploaded rows for `/append`, the queries for an enriched contact bulk match, the domains for `/contacts/discover/generate` (with `find_emails`, every contact a domain can yield counts too) and the personas for DiscoGen persona research (a persona counts twice when its company is included). Retrying at `max_affordable_records` fits. A request with no size to lower (a single-company lookup such as `/bizdata`, `/score`, `/growth`, `/extract` or `/match`, a single email find, or one of the four row-priced lookups) only asks you to top up. MCP tools refuse with the same `message` as a tool error. Asynchronous tasks (bulk match, contact bulk match, segment, DiscoGen, ContaGen, ICP validation, email find) are priced when they are submitted. While they run: - A long DiscoGen, ContaGen or ICP validation task whose balance runs out stops before the next batch and keeps, and bills, the results it finished. - A ContaGen task with `find_emails` prices the email finds against the live balance before submitting them; when they do not fit it skips them, keeps the contacts it found with `email` null, and says so in its warnings. - CRM jobs (contact enrichment, company audit and contact matching) check the request fee when they start and their exact count inside the job, before anything is pushed or billed; a job that does not fit fails with the balance in its error and pushes and bills nothing. - A prospecting run pauses as `needs_input` with `stop_reason` `account_unavailable`. Requests run at the same time are each priced against the same balance, so concurrent requests can together spend slightly past it. ## 422 vs 400 Every rule about the shape of a request (required parameters, lengths, list sizes, numeric ranges, enum values, date formats, URL and UUID formats, unknown parameters) is checked before the request is processed and fails with `422`. The body is the standard FastAPI validation shape: a `detail` array with one entry per failing field, giving the field location, a message, and an error type. Cross-field rules are checked at the same stage and answer `422` as well, with `["query"]` as the location: on `/discover` and `/count`, a `state` that resolves under none of the selected countries (`Value error, Filter state has an invalid value {value}`) and a `state` sent with more than one `country` value (`Value error, State filter cannot be used with multiple countries or without a country`). Contacts state filters resolve the same way but are soft: an unresolvable value is dropped, not rejected. ```json { "detail": [ { "loc": ["query", "max_records"], "msg": "Input should be greater than or equal to 5", "type": "greater_than_equal" } ] } ``` `400` is reserved for checks that can only run once the request is being processed: a value that fails domain normalization (`Invalid Domain`), a check against a stored record (for example changing a provider's endpoint without supplying a new API key), a custom `base_url` that resolves to a private address, an uploaded file that cannot be parsed or exceeds its row limit, and business rules such as an exclusion list that is too small. The body is a plain string: ```json { "detail": "Invalid Domain : hubspot" } ``` If you previously matched on `400` for length, range, or enum failures, match on `422` instead. Each endpoint page lists its validation rules. Endpoints that take query parameters (`/count`, `/discover`, `/contacts`, `/contacts/count`, `/contacts/match`, `/match`, `/bulkmatch`, `/append`, and others) reject unknown parameters with `422` and an `extra_forbidden` detail, so typos are surfaced rather than silently ignored. Documented deprecated aliases (e.g. `exact_match` → `phrase_match`, `vendor` → `tech_stack`, `nl_match` → `icp_text`) continue to work for backward compatibility. --- # Datasets ## BizData Dataset Our core business data feed containing company profiles with firmographic data, contact information, and industry classifications. ## Flat-File Layout | Field | Type | Description | |-------|------|-------------| | `domain` | string | Primary domain | | `name` | string | Company name | | `status` | object | Active/inactive status with confidence | | `score` | integer | Digital footprint score (1-800) | | `start_date` | date | First certificate date (company start estimate) | | `end_date` | date | Last certificate date if closed, empty if active | | `address` | object | Street, city, state, zip, country. `state` is the ISO 3166-2 subdivision name, e.g. `California` | | `latitude` | float | Latitude of the company's location, when resolved | | `longitude` | float | Longitude of the company's location, when resolved | | `geo_precision` | string | How precisely the coordinates were resolved: `rooftop`, `map_pin`, `street`, `zip`, `city`. Null when no coordinates were resolved | | `phones` | array | Phone numbers | | `public_emails` | array | Public email addresses | | `social_urls` | array | Social media profile URLs | | `redirect_domain` | string | Final domain if redirects exist | | `description` | string | Company description | | `keywords` | object | NLP-generated keywords with confidence scores | | `industry_groups` | object | Industry classifications with confidence (see [Industries](https://docs.discolike.com/industries/)) | | `sub_industry` | object | Second-level industry labels as `PARENT/SUB`:confidence pairs (see [Sub-Industries](https://docs.discolike.com/sub-industries/)). Null when the domain was never scored against its category's sub-labels, `{}` when it was scored and nothing matched | | `update_date` | date | Last record update | | `employees` | string | Employee count range: `1-10`, `11-50`, `51-200`, `201-500`, `501-1000`, `1001-5000`, `5001-10000`, `10001+` | | `revenue_range` | string | Revenue bucket: `<1M`, `1-10M`, `10-100M`, `100M-1B`, `>1B`, or `N/A` when unknown | | `business_model` | object | Business model labels with confidence scores: `B2B`, `B2C`, `B2G`, `G2B`, `G2C`, `D2C`, `C2C`, `C2B` | | `mx_provider` | string | Email/MX hosting provider domain (e.g., `google.com`, `microsoft.com`), `no_mx` when the domain has no mail server, or `null` when not resolved | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Certificate Stream Dataset Certificate information as we collect it from Certificate Authorities (CA), full worldwide coverage. ## Flat-File Layout | Name | Format | Description | | ---- | ------ | ----------- | | domain | String | Normalized domain name (excluding any subdomain). | | record_date | Date (YYYY-MM-DD) | Date in which we compiled the record. | | fqdn | String | Fully Qualified Domain Name for the certificate, as appears in the certificate's subject. | | secondary_domains | Array[String] | List of secondary domains and certificates, as appears in the certificate's secondary alternate domains. | | date_from | DateTime (YYYY-MM-DD HH:MM:SS) | Start of certificate validity period. | | date_to | DateTime (YYYY-MM-DD HH:MM:SS) | End of certificate validity period. | | organization | String | Organization information for the certificate, as appears in the certificate's subject. | | city | String | City information for the certificate, as appears in the certificate's subject. | | state | String | State information for the certificate, as appears in the certificate's subject. | | country | String | Country information for the certificate, as appears in the certificate's subject. | | email | String | E-mail information for the certificate, as appears in the certificate's subject. | | sha1 | String | The SHA-1 fingerprint of a certificate (SHA-1 digest value of the certificate's DER representation). | | issuer_country | String | Country information for the issuer of the certificate, as appears in the issuer. | | issuer_organization | String | Organization information for the certificate issuer, as appears in the certificate's issuer. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## DNS Dataset Continuously updated dataset covering all domains that we see through our certificates firehose, including domains referenced in the subject alternative name data field. ## Flat-File Layout | Name | Format | Description | | ---- | ------ | ----------- | | fqdn | String | Fully qualified domain name for which the DNS record was queried. | | domain | String | Domain portion of the FQDN. | | record_type | String | Type of DNS record, currently supported record types: A/MX/TXT/SOA/NS/CNAME/DNAME. | | record_data | Array(String) | DNS data for the specified record type. | | record_date | Date (YYYY-MM-DD) | Date in which the DNS record was queried. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Domain Status Dataset Output of our processing pipeline, from resolving DNS to obtaining site page text including detection of parked domains. ## Flat-File Layout | Name | Format | Description | | ------------- | ----------------- | ------------------------------------------------ | | domain | String | Company domain and unique record identifier. | | record_date | Date (YYYY-MM-DD) | Date in which we compiled the record. | | status_code | UInt8 | Best status code for the record's compiled date. | | status_reason | Nullable(String) | Additional verbose information for status_code. | ## Status Codes | Code | Description | | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 0 | Non-Business or personal domain (portfolio page, personal page, school project, blog, etc.) | | 1 | Business Domain | | 2 | Parked Domain (registrar parked pages, e-commerce parked, hosted parked, etc.) | | 3 | Re-scrape required; Additional re-scraping is necessary to generate a more meaningful status (usually caused by WAF protection or scraping limitations) | | 4 | Re-scrape not required; Identified as a server default page, login page, page with no helpful info, etc. | | 80 | Language not supported; Website is not in a language we currently process. The detected language is in the reason column. | | 81 | Body too short; Content retrieved needs to be longer to be an actual website, despite receiving HTTP status 200 (in some cases, this is triggered by WAF). | | 82 | Scraping unsuccessful; we could not to get website content in this pass. The HTTP error code is in the reason column. | | 90 | NXDOMAIN (domain not exists) or SERVFAIL (DNS server cannot return a result) response returned from the DNS server. Some SERVFAILs are recoverable in future retries. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## PublicLink Dataset The PublicLink dataset identifies relationships between business domains by matching shared public contact information (phone numbers, email addresses, and social media URLs) extracted from their websites. When two domains publish the same phone number, email, or social profile, they are linked as related entities. ## Use Cases - **Corporate group mapping**: Identify brands, subsidiaries, and regional sites owned by the same organization (e.g., `acmehotels.com` and `acmeresorts.com` sharing `privacy@acmehotels.com`) - **M&A and portfolio analysis**: Detect domains that began sharing contact info after an acquisition - **Lead deduplication**: Avoid reaching out to the same organization through multiple domains - **Fraud and spam detection**: Flag networks of domains sharing identical contact details, which can indicate lead-gen farms or templated sites ## Link Sources | Source | How it works | |--------|-------------| | `email` | Identical email addresses extracted from both websites (e.g., `info@company.com` found on two different domains) | | `social` | Identical social media profile URLs extracted from both websites (e.g., the same LinkedIn company page linked from two domains) | | `phone` | Identical phone numbers extracted from both websites | ## Flat-File Layout | Name | Format | Description | | ------------- | ----------------- | ------------------------------------------------------------------------------------------------- | | domain | String | Normalized domain name (excluding any subdomain) | | linked_domain | String | Normalized linked domain name (excluding any subdomain) | | record_date | Date (YYYY-MM-DD) | Date the record was compiled | | source | String | Link method: `email`, `social`, or `phone` | | link_values | Array[String] | The shared values establishing the link (email addresses, phone numbers, or social media URLs) | ## Example Record ```json { "domain": "acmehotels.com", "linked_domain": "acmeresorts.com", "source": "email", "link_values": ["privacy@acmehotels.com", "dpo@acmehotels.com"], "record_date": "2024-07-17" } ``` ## API Access Query PublicLink relationships via the [PublicLink API](https://docs.discolike.com/api/endpoints/publiclink/) or the `public-company-links` MCP tool. Filter by domain and link source. ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Redirect Domains Dataset This dataset identifies and maps domain linkages based on URL redirects, highlighting the connections between domains through their redirection patterns. We capture all forms of redirects, including HTTP 301 and 302, JavaScript redirects, meta tag redirects, and more. ## Flat-File Layout | Name | Format | Description | | ---- | ------ | ----------- | | source_domain | String | Normalized domain name (excluding any subdomain). | | source_fqdn | String | The source FQDN used to establish domain linkages within this dataset. | | linked_domain | String | Normalized linked domain name (excluding any subdomain). | | linked_fqdn | String | The linked FQDN used to establish domain linkages within this dataset. | | record_date | Date (YYYY-MM-DD) | Date in which we compiled the record. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Site Text Dataset Extracted text and site links. Available only in JSON format and as a daily feed. ## Flat-File Layout LinkObject is a key/value combination of Link Text (String): URL (String). | Name | Format | Description | | ---- | ------ | ----------- | | url | String | Page from which the content was extracted. | | domain | String | Normalized domain name (excluding any subdomain). | | extraction_date | Date (YYYY-MM-DD) | Date in which the content was extracted. | | page | String | Classification of the URL from which the content was extracted. Possible values are 'home', 'about', 'careers', 'documentation', 'location', 'policy', 'products', 'services'. | | extracted_text | String | Text as extracted from the URL, cleaned up from all HTML tags and other elements that cannot be used for embeddings. | | extracted_links | Array[LinkObject] | Compilation of URL links and their corresponding or alternative text from <a> tags. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Subsidiaries Linkage Dataset This dataset maps the connections between business domains by leveraging alternative domain names listed in SSL certificates, facilitating the identification of related entities. ## Flat-File Layout | Name | Format | Description | | ---- | ------ | ----------- | | source_domain | String | Normalized domain name (excluding any subdomain). | | source_fqdn | String | The source FQDN used to establish domain linkages within this dataset. | | source_score | Unsigned Int32 | Domain score computed from certificates history, numeric number, values (1 - 800). | | linked_domain | String | Normalized linked domain name (excluding any subdomain). | | linked_fqdn | String | The linked FQDN used to establish domain linkages within this dataset. | | linked_score | Unsigned Int32 | Domain score computed from certificates history, numeric number, values (1 - 800). | | record_date | Date (YYYY-MM-DD) | Date in which we compiled the record. | | parent_domain | String | Domain with the highest score among all source-linked pairs. | | child_domain | String | Remaining domains with scores lower than the parent domain's score. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. ## Vendor Integration Dataset The Vendor Integration Dataset captures information about vendors who have implemented their services under a client's primary domain, utilizing white labeling. ## Flat-File Layout | Name | Format | Description | | ------------- | ----------------- | ------------------------------------------------------------------------------------------------- | | client_domain | String | Normalized domain name (excluding any subdomain). | | client_fqdn | String | The client FQDN (Fully Qualified Domain Name) used for the implementation of the vendor solution. | | vendor_domain | String | Normalized vendor domain name (excluding any subdomain). | | vendor_fqdn | String | The vendor FQDN (Fully Qualified Domain Name) used in the client's white label implementation. | | record_date | Date (YYYY-MM-DD) | Date in which we compiled the record. | ## Delivery Available as JSON or delimited flat files with daily, weekly, or monthly update frequency. --- # Appendix ## Industries **Supported:** 53 industry categories. Companies are classified into one or more of these, each with a confidence score (see the `industry_groups` field in [BizData](https://docs.discolike.com/datasets/bizdata/)). Filter on them with the `category` and `negate_category` parameters on [Discover](https://docs.discolike.com/api/endpoints/discover/). Pass the exact API value (uppercase); matching is case-insensitive. | Industry | API Value | |----------|-----------| | Accounting | `ACCOUNTING` | | Advertising and Marketing | `ADVERTISING_AND_MARKETING` | | Agriculture and Natural Resources | `AGRICULTURE_AND_NATURAL_RESOURCES` | | Alcohol and Tobacco | `ALCOHOL_AND_TOBACCO` | | Automotive | `AUTOMOTIVE` | | Big Data and Analytics | `BIG_DATA_AND_ANALYTICS` | | Biotechnology | `BIOTECHNOLOGY` | | Blockchain and Cryptocurrency | `BLOCKCHAIN_AND_CRYPTOCURRENCY` | | Business Products and Services | `BUSINESS_PRODUCTS_AND_SERVICES` | | Cloud Computing | `CLOUD_COMPUTING` | | Computer Hardware and Semiconductors | `COMPUTER_HARDWARE_AND_SEMICONDUCTORS` | | Conglomerates, Shell & Holding Companies | `CONGLOMERATES_SHELL_AND_HOLDING_COMPANIES` | | Construction | `CONSTRUCTION` | | Consumer Products | `CONSUMER_PRODUCTS` | | Consumer Services | `CONSUMER_SERVICES` | | Cybersecurity | `CYBERSECURITY` | | Defense and Aerospace | `DEFENSE_AND_AEROSPACE` | | E-Commerce | `E-COMMERCE` | | Education | `EDUCATION` | | Energy | `ENERGY` | | Engineering | `ENGINEERING` | | Entertainment | `ENTERTAINMENT` | | Environmental Services | `ENVIRONMENTAL_SERVICES` | | Fashion Textile and Apparel | `FASHION_TEXTILE_AND_APPAREL` | | Financial Services | `FINANCIAL_SERVICES` | | Food and Beverage | `FOOD_AND_BEVERAGE` | | Gaming and Esports | `GAMING_AND_ESPORTS` | | Government Services | `GOVERNMENT_SERVICES` | | Healthcare | `HEALTHCARE` | | Hospitality | `HOSPITALITY` | | Human Resources | `HUMAN_RESOURCES` | | Insurance | `INSURANCE` | | IT Services | `IT_SERVICES` | | Legal | `LEGAL` | | Manufacturing | `MANUFACTURING` | | Media | `MEDIA` | | Mining and Metals | `MINING_AND_METALS` | | Nonprofit and Philanthropy | `NONPROFIT_AND_PHILANTHROPY` | | Oil and Gas | `OIL_AND_GAS` | | Pharmaceuticals | `PHARMACEUTICALS` | | Private Equity and Venture Capital | `PRIVATE_EQUITY_AND_VENTURE_CAPITAL` | | Real Estate | `REAL_ESTATE` | | Renewable Energy | `RENEWABLE_ENERGY` | | Restaurants | `RESTAURANTS` | | Retail | `RETAIL` | | SaaS | `SAAS` | | Security | `SECURITY` | | Software | `SOFTWARE` | | Sports and Recreation | `SPORTS_AND_RECREATION` | | Supply Chain and Procurement | `SUPPLY_CHAIN_AND_PROCUREMENT` | | Telecommunications | `TELECOMMUNICATIONS` | | Travel | `TRAVEL` | | Wellness and Lifestyle | `WELLNESS_AND_LIFESTYLE` | ## Sub-Industries **Supported:** 217 sub-industry labels across 40 of the 53 [industry categories](https://docs.discolike.com/industries/). A sub-industry is a second-level label scoped to its parent category, so it is always addressed as the pair. Filter on them with the `sub_industry` and `negate_sub_industry` parameters on [Discover](https://docs.discolike.com/api/endpoints/discover/) and [Count](https://docs.discolike.com/api/endpoints/count/); they also come back in the `sub_industry` field of every company record, as label:confidence pairs. Pass either the bare label (`ROOFING`) or the qualified key (`CONSTRUCTION/ROOFING`). Matching is case-insensitive. A bare label whose parent category is unambiguous adds that parent to the `category` filter for you. The remaining 13 categories have no sub-industries: the category is already the search term. ## Construction | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Roofing | `CONSTRUCTION/ROOFING` | Roofers and roofing contractors. | | Plumbing | `CONSTRUCTION/PLUMBING` | Plumbers and plumbing contractors. | | Electrical | `CONSTRUCTION/ELECTRICAL` | Electricians and electrical contractors. | | Hvac | `CONSTRUCTION/HVAC` | HVAC and heating and cooling contractors. | | Solar Installation | `CONSTRUCTION/SOLAR_INSTALLATION` | Solar installers. Also carries RENEWABLE_ENERGY. | | General Contracting | `CONSTRUCTION/GENERAL_CONTRACTING` | General contractors and construction managers. | | Home Builder | `CONSTRUCTION/HOME_BUILDER` | Home builders and custom home construction firms. | | Remodeling | `CONSTRUCTION/REMODELING` | Kitchen, bath, and whole-home remodelers. | | Painting | `CONSTRUCTION/PAINTING` | Painting contractors. | | Siding Contractor | `CONSTRUCTION/SIDING_CONTRACTOR` | Siding, stucco, and exterior cladding contractors. | | Window And Door Installer | `CONSTRUCTION/WINDOW_AND_DOOR_INSTALLER` | Window, door, and garage door installers. | | Flooring | `CONSTRUCTION/FLOORING` | Flooring installers and refinishers. | | Concrete And Masonry | `CONSTRUCTION/CONCRETE_AND_MASONRY` | Concrete contractors, masons, and foundation specialists. | | Excavation And Demolition | `CONSTRUCTION/EXCAVATION_AND_DEMOLITION` | Excavation, grading, and demolition contractors. | | Architecture | `CONSTRUCTION/ARCHITECTURE` | Architects and architectural practices. | | Restoration | `CONSTRUCTION/RESTORATION` | Water, fire, and mold damage restoration companies. | ## Healthcare | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Primary And Urgent Care | `HEALTHCARE/PRIMARY_AND_URGENT_CARE` | Primary care practices and urgent care clinics. | | Dentist | `HEALTHCARE/DENTIST` | Dental practices, including orthodontic and cosmetic dentistry. | | Optometrist | `HEALTHCARE/OPTOMETRIST` | Optometrists, ophthalmologists, and eye care clinics. | | Mental Health Practice | `HEALTHCARE/MENTAL_HEALTH_PRACTICE` | Therapists, psychologists, counselors, and psychiatrists. | | Rehab Center | `HEALTHCARE/REHAB_CENTER` | Addiction treatment, rehab, and recovery centers. | | Chiropractor | `HEALTHCARE/CHIROPRACTOR` | Chiropractic clinics. | | Physical Therapy | `HEALTHCARE/PHYSICAL_THERAPY` | Physical and occupational therapy practices. | | Medspa And Dermatology | `HEALTHCARE/MEDSPA_AND_DERMATOLOGY` | Medspas, dermatology, and cosmetic procedure clinics. | | Veterinarian | `HEALTHCARE/VETERINARIAN` | Veterinary clinics and animal hospitals. | | Hospital | `HEALTHCARE/HOSPITAL` | Hospitals, medical centers, and health systems. | | Senior Care | `HEALTHCARE/SENIOR_CARE` | Home health, hospice, assisted living, and nursing facilities. | ## Consumer Services | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | House Cleaning Service | `CONSUMER_SERVICES/HOUSE_CLEANING_SERVICE` | House cleaning and maid services for homes. | | Lawn Care | `CONSUMER_SERVICES/LAWN_CARE` | Lawn care, landscaping, and grounds maintenance. | | Tree Service | `CONSUMER_SERVICES/TREE_SERVICE` | Tree removal, trimming, and arborists. | | Residential Pest Control | `CONSUMER_SERVICES/RESIDENTIAL_PEST_CONTROL` | Residential pest, termite, and wildlife control. | | Moving Company | `CONSUMER_SERVICES/MOVING_COMPANY` | Movers, packing services, and junk removal. | | Handyman | `CONSUMER_SERVICES/HANDYMAN` | Handyman and small home repair services. | | Appliance Repair | `CONSUMER_SERVICES/APPLIANCE_REPAIR` | In-home appliance and household equipment repair. | | Photographer | `CONSUMER_SERVICES/PHOTOGRAPHER` | Wedding, portrait, and event photographers and videographers. | | Wedding Planner | `CONSUMER_SERVICES/WEDDING_PLANNER` | Wedding and event planners and coordinators. | | Hair Salon | `CONSUMER_SERVICES/HAIR_SALON` | Hair salons, barbers, and nail salons. | | Pet Grooming | `CONSUMER_SERVICES/PET_GROOMING` | Pet grooming, boarding, and dog training. | | Funeral Home | `CONSUMER_SERVICES/FUNERAL_HOME` | Funeral homes and cremation services. | ## Legal | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Personal Injury Lawyer | `LEGAL/PERSONAL_INJURY_LAWYER` | Personal injury, car accident, and wrongful death firms. | | Family Lawyer | `LEGAL/FAMILY_LAWYER` | Divorce, custody, and family law firms. | | Criminal Defense Lawyer | `LEGAL/CRIMINAL_DEFENSE_LAWYER` | Criminal defense and DUI firms. | | Estate Planning Lawyer | `LEGAL/ESTATE_PLANNING_LAWYER` | Wills, trusts, estate planning, and probate firms. | | Immigration Lawyer | `LEGAL/IMMIGRATION_LAWYER` | Immigration and visa firms. | | Business Lawyer | `LEGAL/BUSINESS_LAWYER` | Corporate, M&A, and commercial law firms. | | Employment Lawyer | `LEGAL/EMPLOYMENT_LAWYER` | Employment and labor law firms. | | Real Estate Lawyer | `LEGAL/REAL_ESTATE_LAWYER` | Real estate and property law firms. | | Ip Lawyer | `LEGAL/IP_LAWYER` | Patent, trademark, and intellectual property firms. | | Bankruptcy Lawyer | `LEGAL/BANKRUPTCY_LAWYER` | Bankruptcy and debt relief firms. | ## Real Estate | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Real Estate Agency | `REAL_ESTATE/REAL_ESTATE_AGENCY` | Residential real estate agents, teams, and brokerages. | | Commercial Brokerage | `REAL_ESTATE/COMMERCIAL_BROKERAGE` | Commercial real estate brokerages. | | Property Management | `REAL_ESTATE/PROPERTY_MANAGEMENT` | Property management companies. | | Real Estate Developer | `REAL_ESTATE/REAL_ESTATE_DEVELOPER` | Residential, commercial, and mixed-use developers. | | Apartment Operator | `REAL_ESTATE/APARTMENT_OPERATOR` | Apartment communities and multifamily operators. | | Title And Escrow | `REAL_ESTATE/TITLE_AND_ESCROW` | Title, escrow, and closing companies. | | Home Inspector | `REAL_ESTATE/HOME_INSPECTOR` | Home inspectors and property appraisers. | ## Restaurants | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Pizzeria | `RESTAURANTS/PIZZERIA` | Pizza restaurants and pizzerias. | | Coffee Shop | `RESTAURANTS/COFFEE_SHOP` | Coffee shops, cafes, and tea houses. | | Bakery | `RESTAURANTS/BAKERY` | Bakeries, patisseries, and dessert shops. | | Bar And Pub | `RESTAURANTS/BAR_AND_PUB` | Bars, pubs, taprooms, and cocktail lounges. | | Food Truck | `RESTAURANTS/FOOD_TRUCK` | Food trucks and mobile food operators. | | Caterer | `RESTAURANTS/CATERER` | Catering companies and private chefs. | | Fast Food | `RESTAURANTS/FAST_FOOD` | Fast food and quick-service concepts. | ## Automotive | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Car Dealership | `AUTOMOTIVE/CAR_DEALERSHIP` | New, used, and certified pre-owned vehicle dealerships. | | Auto Repair Shop | `AUTOMOTIVE/AUTO_REPAIR_SHOP` | Auto repair shops and service centers. | | Auto Body Shop | `AUTOMOTIVE/AUTO_BODY_SHOP` | Collision repair and auto body shops. | | Tire Shop | `AUTOMOTIVE/TIRE_SHOP` | Tire retailers and wheel service. | | Auto Detailing | `AUTOMOTIVE/AUTO_DETAILING` | Detailing, ceramic coating, tinting, and wraps. | | Towing | `AUTOMOTIVE/TOWING` | Towing, recovery, and roadside assistance. | | Auto Parts Supplier | `AUTOMOTIVE/AUTO_PARTS_SUPPLIER` | Auto parts retailers, distributors, and component suppliers. | ## Retail | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Grocery Store | `RETAIL/GROCERY_STORE` | Grocery stores, supermarkets, and convenience stores. | | Clothing Store | `RETAIL/CLOTHING_STORE` | Multi-brand clothing and footwear stores. | | Furniture Store | `RETAIL/FURNITURE_STORE` | Furniture, mattress, and home goods stores. | | Hardware Store | `RETAIL/HARDWARE_STORE` | Hardware stores, lumber yards, and garden centers. | | Jewelry Store | `RETAIL/JEWELRY_STORE` | Jewelry and watch retailers. | | Pharmacy | `RETAIL/PHARMACY` | Retail pharmacies and drugstores. | ## Wellness And Lifestyle | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Massage Therapy | `WELLNESS_AND_LIFESTYLE/MASSAGE_THERAPY` | Massage therapists and bodywork practices. | | Yoga Studio | `WELLNESS_AND_LIFESTYLE/YOGA_STUDIO` | Yoga and Pilates studios. | | Acupuncture | `WELLNESS_AND_LIFESTYLE/ACUPUNCTURE` | Acupuncture and traditional Chinese medicine. | | Nutritionist | `WELLNESS_AND_LIFESTYLE/NUTRITIONIST` | Dietitians, nutrition counseling, and weight loss programs. | | Life Coach | `WELLNESS_AND_LIFESTYLE/LIFE_COACH` | Life coaches, executive coaches, and personal development. | | Day Spa | `WELLNESS_AND_LIFESTYLE/DAY_SPA` | Day spas and wellness centers. | ## Financial Services | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Bank And Credit Union | `FINANCIAL_SERVICES/BANK_AND_CREDIT_UNION` | Banks and credit unions. | | Mortgage Lender | `FINANCIAL_SERVICES/MORTGAGE_LENDER` | Mortgage lenders, brokers, and loan officers. | | Financial Advisor | `FINANCIAL_SERVICES/FINANCIAL_ADVISOR` | Financial advisors, RIAs, and wealth managers. | | Business Lender | `FINANCIAL_SERVICES/BUSINESS_LENDER` | Business loans, equipment financing, and commercial lending. | | Payment Processor | `FINANCIAL_SERVICES/PAYMENT_PROCESSOR` | Payment processors and merchant services. | | Debt Collection | `FINANCIAL_SERVICES/DEBT_COLLECTION` | Debt collection, credit repair, and credit reporting. | ## Education | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | K12 School | `EDUCATION/K12_SCHOOL` | Public, private, and charter primary and secondary schools. | | University | `EDUCATION/UNIVERSITY` | Colleges, universities, and community colleges. | | Preschool And Daycare | `EDUCATION/PRESCHOOL_AND_DAYCARE` | Preschools, daycare centers, and early learning programs. | | Trade School | `EDUCATION/TRADE_SCHOOL` | Trade schools, vocational training, and apprenticeships. | | Tutoring | `EDUCATION/TUTORING` | Tutoring centers, private tutors, and test prep. | | Driving School | `EDUCATION/DRIVING_SCHOOL` | Driving schools and licensed-skill instruction. | | Online Course Platform | `EDUCATION/ONLINE_COURSE_PLATFORM` | Online course platforms and self-paced learning. | ## Sports And Recreation | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Gym And Fitness | `SPORTS_AND_RECREATION/GYM_AND_FITNESS` | Gyms, health clubs, and fitness studios. | | Personal Training | `SPORTS_AND_RECREATION/PERSONAL_TRAINING` | Personal trainers and small-group coaching. | | Martial Arts | `SPORTS_AND_RECREATION/MARTIAL_ARTS` | Martial arts, boxing, and combat sports schools. | | Dance Studio | `SPORTS_AND_RECREATION/DANCE_STUDIO` | Dance studios, gymnastics, and cheer programs. | | Golf Course | `SPORTS_AND_RECREATION/GOLF_COURSE` | Golf courses and golf clubs. | ## E-Commerce | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Marketplace | `E-COMMERCE/MARKETPLACE` | Multi-seller marketplaces and aggregator platforms. | | Apparel Store | `E-COMMERCE/APPAREL_STORE` | Online clothing, footwear, and accessory sellers. | | Beauty Store | `E-COMMERCE/BEAUTY_STORE` | Online beauty, skincare, and supplement sellers. | | Home Goods Store | `E-COMMERCE/HOME_GOODS_STORE` | Online home goods, furniture, and decor sellers. | ## Advertising And Marketing | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Marketing Agency | `ADVERTISING_AND_MARKETING/MARKETING_AGENCY` | Full-service and digital marketing agencies. | | Seo And Ppc Agency | `ADVERTISING_AND_MARKETING/SEO_AND_PPC_AGENCY` | SEO, paid search, and performance marketing specialists. | | Social Media Agency | `ADVERTISING_AND_MARKETING/SOCIAL_MEDIA_AGENCY` | Social media management and influencer marketing. | | Branding Agency | `ADVERTISING_AND_MARKETING/BRANDING_AGENCY` | Brand strategy, identity, and creative studios. | | Web Design Agency | `ADVERTISING_AND_MARKETING/WEB_DESIGN_AGENCY` | Website design and build shops. | | Pr Agency | `ADVERTISING_AND_MARKETING/PR_AGENCY` | PR and communications firms. | | Lead Generation | `ADVERTISING_AND_MARKETING/LEAD_GENERATION` | Lead generation, cold email outreach, and appointment setting. | ## Manufacturing | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Machine Shop | `MANUFACTURING/MACHINE_SHOP` | CNC machining, precision machining, and machine shops. | | Metal Fabrication | `MANUFACTURING/METAL_FABRICATION` | Sheet metal, welding, and structural fabrication. | | Injection Molding | `MANUFACTURING/INJECTION_MOLDING` | Injection molding and plastics manufacturing. | | Contract Manufacturer | `MANUFACTURING/CONTRACT_MANUFACTURER` | Contract and private-label manufacturers. | | Packaging Manufacturer | `MANUFACTURING/PACKAGING_MANUFACTURER` | Packaging manufacturers and suppliers. | | Robotics And Automation | `MANUFACTURING/ROBOTICS_AND_AUTOMATION` | Robotics, motion control, and factory automation. | ## Saas | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Crm Software | `SAAS/CRM_SOFTWARE` | CRM, sales engagement, and revenue tooling. | | Marketing Software | `SAAS/MARKETING_SOFTWARE` | Marketing automation, CDP, and campaign platforms. | | Developer Tools | `SAAS/DEVELOPER_TOOLS` | Developer platforms, CI/CD, observability, and API tooling. | | Ai Platform | `SAAS/AI_PLATFORM` | AI and ML application platforms and model tooling. | | Analytics Software | `SAAS/ANALYTICS_SOFTWARE` | BI, dashboards, and analytics applications. | | Helpdesk Software | `SAAS/HELPDESK_SOFTWARE` | Helpdesk, ticketing, and customer support platforms. | ## Business Products And Services | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Janitorial | `BUSINESS_PRODUCTS_AND_SERVICES/JANITORIAL` | Commercial cleaning and janitorial contractors. | | Uniform Supplier | `BUSINESS_PRODUCTS_AND_SERVICES/UNIFORM_SUPPLIER` | Uniform supply, rental, and workwear programs. | | Office Supplies | `BUSINESS_PRODUCTS_AND_SERVICES/OFFICE_SUPPLIES` | Office products, supplies, and office furniture. | | Call Center And Bpo | `BUSINESS_PRODUCTS_AND_SERVICES/CALL_CENTER_AND_BPO` | Outsourced contact centers and BPO providers. | | Management Consulting | `BUSINESS_PRODUCTS_AND_SERVICES/MANAGEMENT_CONSULTING` | Management and operations consulting firms. | ## Supply Chain And Procurement | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Freight Broker | `SUPPLY_CHAIN_AND_PROCUREMENT/FREIGHT_BROKER` | Freight brokers and 3PL arrangers. | | Trucking Company | `SUPPLY_CHAIN_AND_PROCUREMENT/TRUCKING_COMPANY` | Trucking carriers, LTL, and FTL operators. | | Warehousing And Fulfillment | `SUPPLY_CHAIN_AND_PROCUREMENT/WAREHOUSING_AND_FULFILLMENT` | Warehousing, 3PL fulfillment, and distribution centers. | | Freight Forwarder | `SUPPLY_CHAIN_AND_PROCUREMENT/FREIGHT_FORWARDER` | Ocean, air, and international freight forwarders. | | Courier | `SUPPLY_CHAIN_AND_PROCUREMENT/COURIER` | Courier, parcel, and last-mile delivery. | | Wholesale Distributor | `SUPPLY_CHAIN_AND_PROCUREMENT/WHOLESALE_DISTRIBUTOR` | Wholesale distributors and importers. | ## Consumer Products | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Beauty Brand | `CONSUMER_PRODUCTS/BEAUTY_BRAND` | Skincare, haircare, cosmetics, and personal care brands. | | Home Goods Brand | `CONSUMER_PRODUCTS/HOME_GOODS_BRAND` | Homeware, kitchenware, and decor brands. | | Pet Products Brand | `CONSUMER_PRODUCTS/PET_PRODUCTS_BRAND` | Pet food, treats, and accessory brands. | | Outdoor Gear Brand | `CONSUMER_PRODUCTS/OUTDOOR_GEAR_BRAND` | Camping, hiking, and outdoor equipment brands. | ## Hospitality | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Hotel | `HOSPITALITY/HOTEL` | Hotels, motels, and inns. | | Resort | `HOSPITALITY/RESORT` | Resorts and all-inclusive properties. | | Vacation Rental | `HOSPITALITY/VACATION_RENTAL` | Vacation rentals, cabins, and B&Bs. | | Campground And Rv Park | `HOSPITALITY/CAMPGROUND_AND_RV_PARK` | Campgrounds, glamping, and RV parks. | | Wedding And Conference Venue | `HOSPITALITY/WEDDING_AND_CONFERENCE_VENUE` | Wedding venues, banquet halls, and conference centers. | ## Travel | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Travel Agency | `TRAVEL/TRAVEL_AGENCY` | Travel agencies and booking platforms. | | Tour Operator | `TRAVEL/TOUR_OPERATOR` | Tour operators and guided experience providers. | | Airline | `TRAVEL/AIRLINE` | Airlines and charter operators. | | Cruise Operator | `TRAVEL/CRUISE_OPERATOR` | Cruise lines and cruise operators. | ## Media | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | News Publisher | `MEDIA/NEWS_PUBLISHER` | Newspapers, news sites, and wire services. | | Magazine Publisher | `MEDIA/MAGAZINE_PUBLISHER` | Consumer magazines and trade publications. | | Book Publisher | `MEDIA/BOOK_PUBLISHER` | Book publishers and imprints. | | Podcast | `MEDIA/PODCAST` | Podcast networks and audio publishers. | | Radio And Tv Station | `MEDIA/RADIO_AND_TV_STATION` | Radio and television broadcasters. | ## Entertainment | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Music Label And Studio | `ENTERTAINMENT/MUSIC_LABEL_AND_STUDIO` | Record labels, recording studios, and music production. | | Event Promoter | `ENTERTAINMENT/EVENT_PROMOTER` | Concert promoters and festival organizers. | | Film Production | `ENTERTAINMENT/FILM_PRODUCTION` | Film and television production companies. | | Talent Agency | `ENTERTAINMENT/TALENT_AGENCY` | Talent agencies, artist management, and booking. | | Nightclub | `ENTERTAINMENT/NIGHTCLUB` | Nightclubs and nightlife venues. | ## Nonprofit And Philanthropy | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Church | `NONPROFIT_AND_PHILANTHROPY/CHURCH` | Churches, synagogues, mosques, temples, and ministries. | | Charity | `NONPROFIT_AND_PHILANTHROPY/CHARITY` | Food banks, shelters, and direct-service charities. | | Foundation | `NONPROFIT_AND_PHILANTHROPY/FOUNDATION` | Private foundations and grantmaking bodies. | | Trade Association | `NONPROFIT_AND_PHILANTHROPY/TRADE_ASSOCIATION` | Trade associations, professional bodies, and member societies. | | Museum | `NONPROFIT_AND_PHILANTHROPY/MUSEUM` | Museums, galleries, and cultural institutions. | ## Fashion Textile And Apparel | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Womens Apparel Brand | `FASHION_TEXTILE_AND_APPAREL/WOMENS_APPAREL_BRAND` | Women's apparel brands and fashion houses. | | Mens Apparel Brand | `FASHION_TEXTILE_AND_APPAREL/MENS_APPAREL_BRAND` | Men's apparel brands. | | Footwear Brand | `FASHION_TEXTILE_AND_APPAREL/FOOTWEAR_BRAND` | Shoe and footwear brands. | | Jewelry Brand | `FASHION_TEXTILE_AND_APPAREL/JEWELRY_BRAND` | Jewelry and watch design houses. | | Custom Apparel And Embroidery | `FASHION_TEXTILE_AND_APPAREL/CUSTOM_APPAREL_AND_EMBROIDERY` | Custom apparel decorators, embroidery, and screen printing. | ## Food And Beverage | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Food Manufacturer | `FOOD_AND_BEVERAGE/FOOD_MANUFACTURER` | Packaged food producers and processors. | | Beverage Brand | `FOOD_AND_BEVERAGE/BEVERAGE_BRAND` | Non-alcoholic beverage brands and bottlers. | | Coffee Roaster | `FOOD_AND_BEVERAGE/COFFEE_ROASTER` | Coffee roasters and tea producers. | | Food Distributor | `FOOD_AND_BEVERAGE/FOOD_DISTRIBUTOR` | Food distributors and foodservice wholesalers. | ## It Services | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Msp | `IT_SERVICES/MSP` | Managed service providers and outsourced IT. | | It Consulting | `IT_SERVICES/IT_CONSULTING` | Technology consulting, systems integration, cloud migration, and DevOps services. | | It Staffing | `IT_SERVICES/IT_STAFFING` | IT staff augmentation and offshore development providers. | ## Engineering | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Civil Engineering Firm | `ENGINEERING/CIVIL_ENGINEERING_FIRM` | Civil and structural engineering consultancies. | | Mechanical Engineering Firm | `ENGINEERING/MECHANICAL_ENGINEERING_FIRM` | Mechanical and thermal engineering services. | | Electrical Engineering Firm | `ENGINEERING/ELECTRICAL_ENGINEERING_FIRM` | Electrical, power systems, and controls engineering. | | Land Surveying | `ENGINEERING/LAND_SURVEYING` | Land surveyors and geotechnical services. | ## Software | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Mobile App Developer | `SOFTWARE/MOBILE_APP_DEVELOPER` | Mobile application developers and studios. | | Custom Software Developer | `SOFTWARE/CUSTOM_SOFTWARE_DEVELOPER` | Bespoke software development firms. | | Engineering Software | `SOFTWARE/ENGINEERING_SOFTWARE` | CAD, CAE, EDA, and simulation software. | ## Human Resources | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Staffing Agency | `HUMAN_RESOURCES/STAFFING_AGENCY` | Temporary and contract staffing agencies. | | Executive Search | `HUMAN_RESOURCES/EXECUTIVE_SEARCH` | Executive search and headhunting firms. | | Peo And Payroll | `HUMAN_RESOURCES/PEO_AND_PAYROLL` | PEOs, payroll bureaus, and employer of record providers. | | Benefits Broker | `HUMAN_RESOURCES/BENEFITS_BROKER` | Benefits brokers and administration providers. | | Hr Consulting | `HUMAN_RESOURCES/HR_CONSULTING` | HR advisory, compliance, and org design consulting. | ## Gaming And Esports | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Game Studio | `GAMING_AND_ESPORTS/GAME_STUDIO` | Game development studios and publishers, including mobile-first studios. | | Esports Organization | `GAMING_AND_ESPORTS/ESPORTS_ORGANIZATION` | Esports orgs, leagues, and tournament operators. | | Online Casino And Betting | `GAMING_AND_ESPORTS/ONLINE_CASINO_AND_BETTING` | Online casino, sportsbook, and betting platforms. | ## Insurance | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Insurance Agency | `INSURANCE/INSURANCE_AGENCY` | Independent insurance agencies and local brokers. | | Insurance Carrier | `INSURANCE/INSURANCE_CARRIER` | Insurance carriers and underwriters. | ## Agriculture And Natural Resources | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Crop Farm | `AGRICULTURE_AND_NATURAL_RESOURCES/CROP_FARM` | Row crop, grain, and produce farming operations. | | Livestock Ranch | `AGRICULTURE_AND_NATURAL_RESOURCES/LIVESTOCK_RANCH` | Cattle, poultry, dairy, and livestock operations. | | Farm Equipment Dealer | `AGRICULTURE_AND_NATURAL_RESOURCES/FARM_EQUIPMENT_DEALER` | Farm machinery dealers and equipment suppliers. | | Agricultural Supplier | `AGRICULTURE_AND_NATURAL_RESOURCES/AGRICULTURAL_SUPPLIER` | Seed, fertilizer, feed, and crop protection suppliers. | ## Blockchain And Cryptocurrency | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Crypto Exchange | `BLOCKCHAIN_AND_CRYPTOCURRENCY/CRYPTO_EXCHANGE` | Crypto exchanges, brokers, and trading venues. | | Crypto Wallet And Custody | `BLOCKCHAIN_AND_CRYPTOCURRENCY/CRYPTO_WALLET_AND_CUSTODY` | Wallets, custodians, and key management. | | Defi Protocol | `BLOCKCHAIN_AND_CRYPTOCURRENCY/DEFI_PROTOCOL` | Lending, DEX, and decentralized finance protocols. | | Nft Platform | `BLOCKCHAIN_AND_CRYPTOCURRENCY/NFT_PLATFORM` | NFT marketplaces and digital collectible platforms. | ## Cybersecurity | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Mssp | `CYBERSECURITY/MSSP` | Managed security providers, SOC-as-a-service, and MDR. | | Penetration Testing | `CYBERSECURITY/PENETRATION_TESTING` | Penetration testing and offensive security firms. | | Security Compliance | `CYBERSECURITY/SECURITY_COMPLIANCE` | SOC 2, ISO, HIPAA, and security compliance advisory. | ## Private Equity And Venture Capital | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Venture Capital Firm | `PRIVATE_EQUITY_AND_VENTURE_CAPITAL/VENTURE_CAPITAL_FIRM` | Early and growth-stage venture funds. | | Private Equity Firm | `PRIVATE_EQUITY_AND_VENTURE_CAPITAL/PRIVATE_EQUITY_FIRM` | Buyout, growth equity, and control-investment firms. | ## Alcohol And Tobacco | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Brewery | `ALCOHOL_AND_TOBACCO/BREWERY` | Breweries, brewpubs, and beer producers. | | Winery | `ALCOHOL_AND_TOBACCO/WINERY` | Wineries and wine producers. | | Distillery | `ALCOHOL_AND_TOBACCO/DISTILLERY` | Distilleries and spirits producers. | | Cannabis Dispensary | `ALCOHOL_AND_TOBACCO/CANNABIS_DISPENSARY` | Cannabis and CBD dispensaries and retailers. | ## Security | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Security Guard Company | `SECURITY/SECURITY_GUARD_COMPANY` | Manned guarding and security officer services. | | Alarm And Access Control | `SECURITY/ALARM_AND_ACCESS_CONTROL` | Alarm monitoring, access control, and surveillance installers. | | Locksmith | `SECURITY/LOCKSMITH` | Locksmiths and physical lock services. | ## Environmental Services | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Waste Management Company | `ENVIRONMENTAL_SERVICES/WASTE_MANAGEMENT_COMPANY` | Waste hauling, dumpster, and disposal operators. | | Recycling Center | `ENVIRONMENTAL_SERVICES/RECYCLING_CENTER` | Recycling processors and material recovery facilities. | | Environmental Consulting | `ENVIRONMENTAL_SERVICES/ENVIRONMENTAL_CONSULTING` | Environmental assessment, remediation, and compliance consulting. | ## Biotechnology | Sub-Industry | API Value | Description | |--------------|-----------|-------------| | Therapeutics Developer | `BIOTECHNOLOGY/THERAPEUTICS_DEVELOPER` | Gene, cell, and novel-modality therapeutics companies. | | Genomics Company | `BIOTECHNOLOGY/GENOMICS_COMPANY` | Genomics, sequencing, and genetic testing companies. | | Lab Equipment Supplier | `BIOTECHNOLOGY/LAB_EQUIPMENT_SUPPLIER` | Research tools, instruments, and reagent suppliers. | ## Languages DiscoLike detects and translates company websites across these 55 languages. Filter on them with the `language` and `negate_language` parameters on [Discover](https://docs.discolike.com/api/endpoints/discover/) using the two-letter code. | Code | Language | |------|----------| | `en` | English | | `sq` | Albanian | | `ar` | Arabic | | `az` | Azerbaijani | | `eu` | Basque | | `bn` | Bengali | | `bs` | Bosnian | | `bg` | Bulgarian | | `my` | Burmese | | `ca` | Catalan | | `zh` | Chinese (Simplified) | | `zt` | Chinese (Traditional) | | `hr` | Croatian | | `cs` | Czech | | `da` | Danish | | `nl` | Dutch | | `eo` | Esperanto | | `et` | Estonian | | `fi` | Finnish | | `fr` | French | | `gl` | Galician | | `de` | German | | `el` | Greek | | `he` | Hebrew | | `hi` | Hindi | | `hu` | Hungarian | | `id` | Indonesian | | `ga` | Irish | | `it` | Italian | | `ja` | Japanese | | `ko` | Korean | | `ky` | Kyrgyz | | `lv` | Latvian | | `lt` | Lithuanian | | `mk` | Macedonian | | `ms` | Malay | | `nb` | Norwegian Bokmål | | `fa` | Persian | | `pl` | Polish | | `pt` | Portuguese | | `pb` | Portuguese (Brazilian) | | `ro` | Romanian | | `ru` | Russian | | `sr` | Serbian | | `sk` | Slovak | | `sl` | Slovenian | | `es` | Spanish | | `sw` | Swahili | | `sv` | Swedish | | `tl` | Tagalog | | `th` | Thai | | `tr` | Turkish | | `uk` | Ukrainian | | `ur` | Urdu | | `vi` | Vietnamese |