Skip to content

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, which does the HTTP work and owns credential resolution.

ItemValue
Python3.10 or newer
Distributiondiscolike-cli
Command installeddiscolike
Dependenciesdiscolike==0.3.0 (pinned exactly, released together), typer>=0.12, rich>=13.0
Current version0.3.0
Terminal window
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:

Terminal window
pip install "discolike[cli]"

Confirm the install and see both version numbers at once:

Terminal window
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.

These options belong to the root command and must appear before the subcommand:

Terminal window
discolike --api-key dk_... count --country US
FlagTypeDefaultEnvironment variableDescription
--api-keyTEXTnoneDISCOLIKE_API_KEYAPI key. Overrides the config file saved by discolike auth login.
--base-urlTEXThttps://api.discolike.com/v1noneAPI base URL.
--versionflagnonenonePrint CLI and SDK versions and exit.
--install-completionflagnonenoneInstall completion for the current shell.
--show-completionflagnonenoneShow completion for the current shell, to copy it or customize the installation.
--helpflagnonenoneShow 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.

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.

    Terminal window
    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:

    {"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.

    To use an API key instead, create one at 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:

    Terminal window
    discolike auth login --api-key dk_...
    Terminal window
    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.

    Terminal window
    discolike auth status
    {
    "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.

    Terminal window
    discolike auth logout
    {"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.

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.

The SDK resolves the credential on every command and uses the first source that yields one:

OrderSourceHow to set it
1The --api-key global flagdiscolike --api-key dk_... count --country US
2The DISCOLIKE_API_KEY environment variableexport DISCOLIKE_API_KEY=dk_...
3The config file written by auth login, holding an OAuth session or an API keydiscolike 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.

ItemValue
Path${XDG_CONFIG_HOME:-~/.config}/discolike/config.json
ContentsOne of the two shapes below, selected by auth_method
Permissions0600, owner read/write only
{"auth_method": "api_key", "api_key": "dk_..."}
{
"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.

With a key in place, the cheapest useful command is a count, which returns a single number and bills nothing per record:

Terminal window
discolike count --country US --category software
{
"count": 68900
}

From here:

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.

There are two formats: pretty-printed JSON and a Rich-rendered table. --format selects between them:

ValueResult
tableRender a table if the payload qualifies; otherwise JSON
jsonJSON
anything elseJSON
omittedTable 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.

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.

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:

Terminal window
discolike count --country US --category software --format table
{
"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:

RuleBehavior
Column selectionA 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 limitThe first 8 qualifying columns
Cell truncationValues longer than 80 characters are cut and suffixed with
Null cellsRendered 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.

StreamCarries
stdoutCommand results, either JSON or the rendered table
stderrError payloads, --wait progress lines, and the auth login success payload

Errors are written as a single line of JSON to stderr:

{"error": "AuthenticationError", "message": "Invalid API Key or Session", "status_code": 401}
FieldTypeNotes
errorstringThe exception class name
messagestringThe server’s message, extracted from the response body
status_codeinteger or nullThe HTTP status, or null when the failure never reached the server
retry_afternumberPresent only on a RateLimitError, and only when the response carried a numeric Retry-After header

Nothing is written to stdout when a command fails, so cmd > out.json leaves an empty file rather than a half-written one:

Terminal window
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 codeConditionHTTP status
0Successnone
1Server error, or any unmapped failure5xx, or any other status
1Browser login failure (LoginError): timeout, denied consent, or an invalid callbacknone
2Validation error (client-side or server-side), or a command-line usage error400, 422, or none
3Authentication error401
3Plan access error402, 403
4Rate limited429
5Network or connection failurenone
6Not found404

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:

Terminal window
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:

Terminal window
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.

Because non-TTY output is always JSON, no flag is needed:

Terminal window
discolike count --country US --category software | jq -r '.count'
68900

Field extraction from a list-returning command works the same way:

Terminal window
discolike discover --icp-prompt "B2B payments infrastructure" --max-records 25 \
| jq -r '.[].domain'

Branch on the class of failure rather than on the message text:

#!/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.

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:

{
"task_id": "0f2c...",
"task_family": "bulkmatch",
"hint": "poll with: discolike discogen status 0f2c... --family bulkmatch"
}

Capture the id and poll on your own schedule:

Terminal window
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:

Terminal window
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.

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.

Terminal window
discolike discover --icp-prompt "B2B payments" --param min_similarity=200

Rules:

RuleBehavior
SplittingSplit on the first =. The value may contain further = characters.
ListsA value containing a comma becomes a list. --param social=linkedin,youtube sends social=linkedin&social=youtube.
RepeatablePass --param more than once for multiple parameters.
PrecedenceA named flag overrides --param for the same key.
MalformedA value with no = raises a usage error and exits 2.
Unknown keyA 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 valueA 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:

Terminal window
discolike count --country US --param country=DE --category software
{
"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:

Terminal window
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.

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.

Every example assumes DISCOLIKE_API_KEY is set or discolike auth login has been run; see Authentication.

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.

    Terminal window
    discolike count --category SAAS --country US
    {
    "count": 248818
    }
  2. Add a size band. --employee-range takes min,max, with a comma, not a dash.

    Terminal window
    discolike count --category SAAS --country US --employee-range 51,200
    {
    "count": 9575
    }
  3. Add an intent signal. --phrase-match requires the phrase to appear on the company website.

    Terminal window
    discolike count --category SAAS --country US --employee-range 51,200 \
    --phrase-match "SOC 2"
    {
    "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.

    Terminal window
    discolike count --category SAAS --country US --employee-range 51,200 \
    --tech-stack hubspot.com
    {
    "count": 5097
    }

A bare product name is rejected with exit code 2:

Terminal window
discolike count --category SAAS --tech-stack hubspot
{"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 for the full filter vocabulary.

Build a target list from an ICP description and write it to CSV

Section titled “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.

Terminal window
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:

Terminal window
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:

Terminal window
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 or contact discovery.

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.

Terminal window
cat seed.csv
domain
stripe.com
linear.app
Terminal window
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:

[
{
"domain": "stripe.com",
"input:domain": "stripe.com",
"name": "Stripe",
"status": { "status": "active", "confidence": 0.89 },
"score": 701,
"start_date": "2011-01-04",
"address": {
"street": "354 Oyster Point Blvd",
"city": "South San Francisco",
"state": "CA",
"zip": "94080",
"country": "US"
},
"phones": ["+16504279276", "+18889262289"],
"public_emails": ["sales@stripe.com", "..."],
"..."
},
"..."
]

To get flat CSV back instead of nested JSON, pass --csv and --output:

Terminal window
discolike append seed.csv --dataset bizdata --domain-column domain \
--csv --output enriched.csv
{
"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 for the dataset catalogue.

Match a messy CRM export of company names to domains

Section titled “Match a messy CRM export of company names to domains”

For a single name, match is synchronous. Use the address flags to disambiguate.

Terminal window
discolike match "Stripe" --country US
{
"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": "CA", "country": "US" },
"..."
}
]
}

Pull just the winner:

Terminal window
discolike match "Stripe" --country US | jq -r '.matches[0] | {domain, name, score}'
{
"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:

Terminal window
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:

Terminal window
discolike match --file crm-accounts.csv --name-column company_name
{
"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 and Job.

Find contacts at a set of companies, then get their emails

Section titled “Find contacts at a set of companies, then get their emails”

Size the contact pool first; contacts count is the cheap probe.

Terminal window
discolike contacts count --domain stripe.com --seniority vp --has-email
{
"count": 56
}

Then pull the records. --has-email restricts results to contacts that already carry a verified address.

Terminal window
discolike contacts search --domain linearb.io --seniority vp --has-email --max-records 20 \
> contacts.json
jq -r '.[] | [.name, .title, .email, .email_validated] | @tsv' contacts.json
Lior Shlezinger VP Finance lior@linearb.io true
Jessica Miller VP of People & Culture jess@linearb.io true
Ilan Rado Vice President of Product Management ilan.rado@linearb.io true
Craig Zelley VP, Revenue Operations craig.zelley@linearb.io true

Repeat --domain to cover a whole account list. To drive it from a file of domains:

Terminal window
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
Terminal window
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:

Terminal window
discolike email find Patrick Collison stripe.com --wait --timeout 120
{
"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 <job_id> or discolike email results <batch_id>. See email commands, email, and the Email Find endpoint.

Run a DiscoGen research job and poll it to completion

Section titled “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:

Terminal window
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
{
"task_id": "...",
"task_family": "discogen",
"hint": "poll with: discolike discogen status {id} --family {family}"
}
Terminal window
discolike discogen status <task_id>
discolike discogen cancel <task_id>

Or block in one call:

Terminal window
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 generatediscogen (default)
discolike match --filebulkmatch
discolike contacts bulk-matchcontactmatch
discolike segmentsegment
Terminal window
discolike discogen status <task_id> --family bulkmatch

An unknown family exits 2. See DiscoGen for the research parameters and Job for the Python equivalent.

Stop re-surfacing companies you already own

Section titled “Stop re-surfacing companies you already own”

Save your existing customers as an exclusion list once, then attach it to every discover run.

--domain is repeatable, so build the argument list from a file rather than typing it out:

Terminal window
args=()
while read -r d; do args+=(--domain "$d"); done < customers.txt
discolike queries create-exclusion-list \
--name "Existing customers" \
"${args[@]}" \
--tag customers
{
"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:

Terminal window
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
Terminal window
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:

Terminal window
discolike queries list --tag customers
discolike queries update <query_id> --name "Existing customers (2026)" --tag customers

Persist the domains you already surfaced so the next run can exclude them, or so a teammate can pick up the same set.

Terminal window
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
{
"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:

Terminal window
discolike discover --icp-prompt "..." --country US --max-records 5 \
--exclusion-query-id a848a58a-d0f3-4f5a-9003-768de7460e89

Clean up when a list is stale:

Terminal window
discolike queries delete a848a58a-d0f3-4f5a-9003-768de7460e89
{
"deleted": "a848a58a-d0f3-4f5a-9003-768de7460e89"
}

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.

CodeMeaning
0Success
1Server error, any unmapped failure, or a failed browser login (LoginError)
2Validation 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
3Authentication (401) or plan access (402/403)
4Rate limited (429)
5Connection failure
6Not found (404)

Verified:

Terminal window
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:

{"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.

#!/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 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 for the table rules.

Start the job, keep the id, and poll from anywhere:

Terminal window
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.

The CLI and the Python SDK cover the same API. Reach for Python when you need:

TaskIn Python
Fanning out across hundreds of domainsAsyncDiscolike runs requests concurrently; see Async.
Custom retry or backoff policyConfigure max_retries and timeout on the client; see The client.
Joining several jobs togetherJob.wait() and typed result models, instead of parsing JSON in bash; see Job.

Start at Getting Started for the library equivalent of every command on this page.

Typer’s built-in completion support is enabled. Install it for the shell you are running:

Terminal window
discolike --install-completion

To inspect the script first, or to install it somewhere of your own choosing:

Terminal window
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.