Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.3.2 (2026-09-02)

- CLI: every SDK request field now has a flag — `discover`/`count` gain `--variance`, `--min-similarity`, `--consensus`, `--inclusion-query-id`, `--language`, `--social`, `--subdomain`, `--start-date`, `--redirect`, `--exclude-leadgen` and the `--auto-*` toggles; contacts `search`/`count`/`discover` gain the full filter set; `match` gains per-column flags for file mode and `--min-match-confidence`; `append`/`segment` take `--query-id`; `extract` accepts `--domain`. Dict-typed fields stay `--param` only.
- SDK: `CountParams.exclude_leadgen` now defaults to `False`, matching the platform's `/count` default — counts no longer drop suspected lead-gen sites unless you ask for it. Discover is unchanged.

## 0.3.1 (2026-09-02)

- SDK: `discolike.signup()` / `discolike.async_signup()` create a DiscoLike account for a person from their work email and name, with no credential required. Returns `SignupResult` with the `next_step` text to relay.
Expand Down
30 changes: 18 additions & 12 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
# Examples
# Cookbook

Runnable, self-contained scripts showing how to use the DiscoLike Python SDK for common GTM workflows: matching a messy CRM export to DiscoLike contacts, finding verified work emails in bulk, and discovering plus AI-enriching target accounts. Each script is stdlib-plus-SDK only, has an argparse CLI, and is meant to be copied into your own pipeline and adapted.

| Script | What it does |
|---|---|
| [`match_crm_contacts.py`](match_crm_contacts.py) | Match a CSV of CRM contacts to DiscoLike persona IDs via `contacts.bulk_match()`, with dual domain keys (website + email domain), resumable JSONL checkpointing, and a persona_id + match_score output CSV |
| [`find_emails_from_csv.py`](find_emails_from_csv.py) | Find work emails for a CSV of people (first name, last name, domain) via `email.find_batch()` in chunks of 500; only status "found" bills |
| [`discover_and_enrich.py`](discover_and_enrich.py) | Discover companies matching an ICP with `client.discover()`, then run a DiscoGen research prompt over them with `discogen.process()` and `job.wait()` |

## Running
Runnable scripts for common GTM workflows on the [DiscoLike API](https://discolike.com/api/), written against the [Python SDK](https://docs.discolike.com/sdk/). Each one is stdlib plus `discolike`, takes its inputs on the command line, and reads your key from `DISCOLIKE_API_KEY`.

```bash
pip install discolike
pip install "discolike[cli]"
export DISCOLIKE_API_KEY="dl_..." # create one at https://app.discolike.com/account/management/keys
python examples/<script>.py --help
```

| Script | What it does | How to run |
|---|---|---|
| [`tam_from_seed_domains.py`](tam_from_seed_domains.py) | Counts companies in a country and employee bucket, then discovers lookalikes of three seed domains and writes them to CSV | `python examples/tam_from_seed_domains.py stripe.com adyen.com checkout.com US 51,200 --max-records 100 --output tam.csv` |
| [`icp_prompt_search.py`](icp_prompt_search.py) | Turns a plain-English ICP into a company list with similarity scores | `python examples/icp_prompt_search.py "Series A fintechs in Europe selling to SMBs" --country EU` |
| [`phrase_match_count.py`](phrase_match_count.py) | Counts sites whose text contains an exact phrase, then lists them | `python examples/phrase_match_count.py "SOC 2" --country US` |
| [`enrich_crm_export.py`](enrich_crm_export.py) | Adds firmographics (size, revenue, location, industry, business model) to every domain in a CSV | `python examples/enrich_crm_export.py accounts.csv --domain-column website --output enriched.csv` |
| [`contacts_at_results.py`](contacts_at_results.py) | Discovers companies for an ICP, then finds contacts at the top N filtered by seniority, department, or title | `python examples/contacts_at_results.py "B2B SaaS selling to sales teams" --top 10 --seniority executive --has-email` |
| [`agent_signup_to_first_search.py`](agent_signup_to_first_search.py) | Opens a DiscoLike account for a person from an agent, relays `next_step`, and runs a first search once `DISCOLIKE_API_KEY` is set | `python examples/agent_signup_to_first_search.py --email jane@acme.com --first-name Jane --last-name Doe` |
| [`discover_and_enrich.py`](discover_and_enrich.py) | Discovers companies for an ICP, then runs a DiscoGen research prompt over them (needs a BYOK LLM provider) | `python examples/discover_and_enrich.py --icp "Cybersecurity for SMBs" --country US --query "What is their pricing model?"` |
| [`find_emails_from_csv.py`](find_emails_from_csv.py) | Finds verified work emails for a CSV of first name, last name, domain in batches of 500; only status `found` bills | `python examples/find_emails_from_csv.py people.csv --output emails.csv` |
| [`match_crm_contacts.py`](match_crm_contacts.py) | Matches a messy CRM contact export to DiscoLike persona IDs with resumable checkpointing | `python examples/match_crm_contacts.py contacts.csv --output matched.csv` |
| [`cli_recipes.sh`](cli_recipes.sh) | The same searches as `discolike discover`, `discolike count`, `discolike contacts search`, and `discolike signup` one-liners | `bash examples/cli_recipes.sh` |

Every script prints `--help`. Employee ranges are `min,max` strings such as `51,200`; countries are ISO-2 codes or region aliases like `EU`, `DACH`, `APAC`.
71 changes: 71 additions & 0 deletions examples/agent_signup_to_first_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Open a DiscoLike account for a person, then run a first search once their key exists.

signup() posts {email, first_name, last_name, agent} to https://api.discolike.com/v1/public/signup with no
auth header and returns a next_step message to relay to the person. No API key comes back: the person
confirms their email, logs in at https://app.discolike.com, and creates a key under Account > API keys.
Export that key as DISCOLIKE_API_KEY and rerun this script to make the first discover call.
"""

from __future__ import annotations

import argparse
import os

from discolike import Discolike
from discolike import DiscolikeError
from discolike import signup
from discolike.requests import DiscoverParams

AGENT_NAME = "cookbook"
API_KEY_ENV = "DISCOLIKE_API_KEY"
FIRST_SEARCH_PROMPT = "B2B SaaS companies selling to sales teams"
FIRST_SEARCH_RECORDS = 5


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--email", help="Work email of the person the account is for")
parser.add_argument("--first-name", help="Their first name")
parser.add_argument("--last-name", help="Their last name")
parser.add_argument("--allow-new-email", action="store_true", help="Sign up a second email from this machine")
return parser.parse_args()


def run_signup(args: argparse.Namespace) -> None:
try:
result = signup(
email=args.email,
first_name=args.first_name,
last_name=args.last_name,
agent=AGENT_NAME,
allow_new_email=args.allow_new_email,
)
except DiscolikeError as exc:
print(f"Signup failed: {exc}")
return
print(f"Signup {result.status} for {result.email} (org {result.org_domain}: {result.org_status})")
print(f"Next step: {result.next_step}")


def run_first_search() -> None:
client = Discolike()
companies = client.discover(DiscoverParams(icp_prompt=FIRST_SEARCH_PROMPT, max_records=FIRST_SEARCH_RECORDS))
print(f"\nFirst search, {FIRST_SEARCH_PROMPT!r}:")
for company in companies:
print(f" {company.domain or '':<32} {company.name or ''}")


def main() -> None:
args = parse_args()
if args.email and args.first_name and args.last_name:
run_signup(args)
else:
print("No --email/--first-name/--last-name given, skipping signup.")
if os.environ.get(API_KEY_ENV):
run_first_search()
else:
print(f"\n{API_KEY_ENV} is not set; export it after email confirmation and rerun for the first search.")


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions examples/cli_recipes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# The same searches as the Python scripts, as `discolike` CLI one-liners.
# Install: pip install discolike-cli Auth: export DISCOLIKE_API_KEY="dl_..." or `discolike auth login`
set -euo pipefail

# tam_from_seed_domains.py: count the country + size bucket, then discover lookalikes of three seed domains
discolike count --country US --employee-range 51,200 --format json
discolike discover --domain stripe.com --domain adyen.com --domain checkout.com \
--country US --employee-range 51,200 --max-records 100 --format json

# icp_prompt_search.py: natural-language ICP, domain + name + similarity
discolike discover --icp-prompt "Series A fintechs in Europe selling to SMBs" --country EU --max-records 25 --format json \
| jq -r '.[] | [.domain, .name, .similarity] | @tsv'

# phrase_match_count.py: sites whose text contains an exact phrase, count then list
discolike count --phrase-match "SOC 2" --country US --format json
discolike discover --phrase-match "SOC 2" --country US --max-records 50 --format json | jq -r '.[].domain'

# contacts_at_results.py: discover companies, then executives at the first ten of them
discolike discover --icp-prompt "B2B SaaS companies selling to sales teams" --max-records 10 --format json \
| jq -r '.[].domain' \
| xargs -I{} printf -- '--domain %s ' {} \
| xargs discolike contacts search --seniority executive --has-email --format json

# enrich_crm_export.py: one company profile by domain
discolike company data stripe.com --format json

# agent_signup_to_first_search.py: open an account for a person, no auth needed
discolike signup --email jane@acme.com --first-name Jane --last-name Doe --agent cookbook
58 changes: 58 additions & 0 deletions examples/contacts_at_results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Discover companies for an ICP, then find decision makers at the top N by seniority, department, or title."""

from __future__ import annotations

import argparse

from discolike import Discolike
from discolike.requests import ContactsSearchParams
from discolike.requests import DiscoverParams

SENIORITIES = ["executive", "vp", "director", "manager", "senior_ic", "mid_level", "entry_level"]


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("prompt", help="Natural-language ICP for the company search")
parser.add_argument("--country", action="append", help="ISO-2 country code or region alias, repeatable")
parser.add_argument("--top", type=int, default=10, help="Companies to search contacts at (default 10)")
parser.add_argument("--seniority", action="append", choices=SENIORITIES, help="Persona seniority, repeatable")
parser.add_argument("--department", action="append", help="Persona department, e.g. 'Sales - Marketing'")
parser.add_argument("--title", action="append", help="Job title term, e.g. 'Head of Growth', repeatable")
parser.add_argument("--per-company", type=int, default=3, help="Contacts per company (default 3)")
parser.add_argument("--has-email", action="store_true", help="Only contacts with an email address")
return parser.parse_args()


def main() -> None:
args = parse_args()
client = Discolike()

companies = client.discover(
DiscoverParams(icp_prompt=args.prompt, country=args.country, max_records=max(args.top, 5))
)
domains = [company.domain for company in companies[: args.top] if company.domain]
print(f"Top {len(domains)} companies: {', '.join(domains)}")

contacts = client.contacts.search(
ContactsSearchParams(
domain=domains,
seniority=args.seniority,
department=args.department,
title=args.title,
has_email=args.has_email,
results_by_company=args.per_company,
max_records=max(len(domains) * args.per_company, 20),
)
)
print(f"\n{'domain':<28} {'name':<28} {'title':<40} email")
for contact in contacts:
print(
f"{contact.domain or '':<28} {(contact.name or '')[:28]:<28} "
f"{(contact.title or '')[:40]:<40} {contact.email or ''}"
)
print(f"\n{len(contacts)} contacts across {len({contact.domain for contact in contacts})} companies")


if __name__ == "__main__":
main()
84 changes: 84 additions & 0 deletions examples/enrich_crm_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Enrich a CSV of domains with firmographics from the company profile endpoint and write a widened CSV."""

from __future__ import annotations

import argparse
import csv
import sys
from pathlib import Path

from discolike import Discolike
from discolike import DiscolikeError
from discolike.requests import CompaniesDataParams

ENRICHED_FIELDS = [
"dl_name",
"dl_employees",
"dl_revenue_range",
"dl_country",
"dl_state",
"dl_city",
"dl_industry_groups",
"dl_business_model",
"dl_description",
"dl_error",
]


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path, help="CSV with a domain column")
parser.add_argument("--output", type=Path, default=Path("enriched.csv"), help="Output CSV path")
parser.add_argument("--domain-column", default="domain", help="Column holding the domain (default: domain)")
return parser.parse_args()


def top_keys(weights: dict[str, float], *, limit: int = 3) -> str:
return "; ".join(key for key, _ in sorted(weights.items(), key=lambda item: item[1], reverse=True)[:limit])


def enrich_row(client: Discolike, *, domain: str) -> dict[str, str]:
try:
profile = client.companies.data(CompaniesDataParams(domain=domain))
except DiscolikeError as exc:
return {"dl_error": str(exc)}
address = profile.address
return {
"dl_name": profile.name or "",
"dl_employees": profile.employees or "",
"dl_revenue_range": profile.revenue_range or "",
"dl_country": address.country if address else "",
"dl_state": address.state if address else "",
"dl_city": address.city if address else "",
"dl_industry_groups": top_keys(profile.industry_groups),
"dl_business_model": top_keys(profile.business_model),
"dl_description": profile.description or "",
"dl_error": "",
}


def main() -> None:
args = parse_args()
with args.input.open(newline="", encoding="utf-8-sig") as handle:
reader = csv.DictReader(handle)
fieldnames = list(reader.fieldnames or [])
rows = list(reader)
if args.domain_column not in fieldnames:
sys.exit(f"Column {args.domain_column!r} not found in {args.input}; columns are {fieldnames}")

client = Discolike()
enriched = 0
with args.output.open(mode="w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames + ENRICHED_FIELDS, extrasaction="ignore")
writer.writeheader()
for row in rows:
domain = row[args.domain_column].strip().lower().removeprefix("www.")
extra = enrich_row(client, domain=domain) if domain else {"dl_error": "empty domain"}
if not extra.get("dl_error"):
enriched += 1
writer.writerow({**row, **extra})
print(f"Enriched {enriched}/{len(rows)} rows -> {args.output}")


if __name__ == "__main__":
main()
33 changes: 33 additions & 0 deletions examples/icp_prompt_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Describe your ideal customer in plain English and print the matching companies with their similarity score."""

from __future__ import annotations

import argparse

from discolike import Discolike
from discolike.requests import DiscoverParams


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("prompt", help="Natural-language ICP, e.g. 'Series A fintechs in Europe selling to SMBs'")
parser.add_argument("--country", action="append", help="ISO-2 country code or region alias, repeatable")
parser.add_argument("--max-records", type=int, default=25, help="Companies to return (5-10000, default 25)")
return parser.parse_args()


def main() -> None:
args = parse_args()
client = Discolike()
companies = client.discover(
DiscoverParams(icp_prompt=args.prompt, country=args.country, max_records=args.max_records)
)
print(f"{'domain':<32} {'name':<40} similarity")
for company in companies:
similarity = f"{company.similarity:.0f}" if company.similarity is not None else "-"
print(f"{company.domain or '':<32} {(company.name or '')[:40]:<40} {similarity}")
print(f"\n{len(companies)} companies for: {args.prompt!r}")


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions examples/phrase_match_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Find companies whose website contains an exact phrase: count the match first, then pull the list."""

from __future__ import annotations

import argparse

from discolike import Discolike
from discolike.requests import CountParams
from discolike.requests import DiscoverParams


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("phrase", nargs="+", help="Phrases the site must contain (up to 20, 3+ chars each)")
parser.add_argument("--country", action="append", required=True, help="ISO-2 country code or region alias")
parser.add_argument("--max-records", type=int, default=50, help="Companies to return (5-10000, default 50)")
return parser.parse_args()


def main() -> None:
args = parse_args()
client = Discolike()

total = client.count(CountParams(phrase_match=args.phrase, country=args.country))
print(f"{total.count} sites in {', '.join(args.country)} mention {args.phrase}")

companies = client.discover(
DiscoverParams(phrase_match=args.phrase, country=args.country, max_records=args.max_records)
)
for company in companies:
print(f"{company.domain or '':<32} {company.name or ''}")
print(f"\nShowing {len(companies)} of {total.count}")


if __name__ == "__main__":
main()
Loading
Loading