Code & Snippets

PitchBook Company Enrichment.

One-shot PitchBook enrichment: investors, geography, round type, total raised, last round size & valuation — with USD enforcement and a configurable top-investor watchlist.

Sourcing
Python · 221 lines
C.1

Download

Grab the source file. Drop it into your service, set the required environment variables, and deploy.

C.2

How it works

The gist

Resolve a company's PitchBook ID from its Attio name/website, then pull the fields that actually matter for triage: investor list, HQ geography, most recent round type and date, total raised, last round size, and last round valuation. Optionally cross-check the investor list against your firm's top-investor watchlist.

The flow

  1. Inputsapi_key, company_name, company_website via input_data (Zapier "Run Python" convention).
  2. Resolve PB ID via /search, preferring COMPANY records and falling back to the first result.
  3. Investors/companies/{id}/investors, deduplicated.
  4. Top-investor match — case-insensitive lookup against TOP_INVESTORS with an ALIASES map for noisy spellings.
  5. Geography/companies/{id}/bio → city, state, country.
  6. Most recent financing/companies/{id}/most-recent-financing for round type and date.
  7. Funding amountstotalMoneyRaised, lastFinancingSize, lastFinancingValuation (or lastKnownValuation as fallback).
  8. Return a flat dict with every field present, so downstream Zap steps can write straight into Attio without null-checking.

Smart touches worth knowing

  • USD enforcement. assert_usd raises on any non-USD currency so you never silently mix EUR/GBP amounts into the CRM.
  • Graceful no-data path. If search returns nothing or the ID can't be found, no_data_output() returns the same shape with data_status: "no data" — Zaps don't break, they just route differently.
  • safe_get wraps every call so a single 5xx on one endpoint doesn't take down the whole enrichment.

Configuring TOP_INVESTORS

The TOP_INVESTORS set ships empty — populate it with your firm's own watchlist (canonical spellings, matched to how investors appear in your CRM), and use ALIASES to map PitchBook's alternate spellings back to your canonical names. Don't need a watchlist? Leave it empty — the rest of the enrichment still runs and matched_investors will come back as "None".

Why this exists

Once a company lands in Attio, this is the single call that hydrates the record with everything you need to decide whether it's worth a human's attention — investors, money, geo, recency — in one round trip.

C.3

Used in this automation

This snippet powers the end-to-end automation below. Open it for the full tool chain, prompts, and job-to-be-done context.

C.4

Source

Full source, exactly as shipped. The download above is byte-identical.

pitchbook_enrich.pyPython
"""
Enrich a company from PitchBook

Designed to run inside a Zapier "Run Python" step (or any environment that
exposes `input_data`). Given a company's name and/or website from Attio,
resolve the PitchBook ID, then pull:

  - Investor list (deduplicated)
  - Optional match against YOUR firm's top-investor watchlist
  - HQ geography (city, state, country)
  - Most recent financing round type
  - Total funding raised, last round size, last round valuation, last round date

All money fields are USD-enforced — the script raises if PitchBook returns
a non-USD currency so you don't silently mix currencies in the CRM.

# ──────────────────────────────────────────────────────────────────────
# TOP_INVESTORS watchlist
#
# Populate the TOP_INVESTORS set below with your own firm's preferred
# co-investors (one canonical spelling each, matching how they appear in
# your CRM). Add common alternate spellings to ALIASES so noisy PitchBook
# strings map back to your canonical names.
#
# Don't need the watchlist? Just leave TOP_INVESTORS empty — the rest of
# the enrichment (investors, geo, funding, valuation, round type) still
# works; `matched_investors` will simply come back as "None".
# ──────────────────────────────────────────────────────────────────────
"""

import requests

api_key = input_data['api_key']                      # PB API key
company_name = input_data.get('company_name')        # from Attio
company_website = input_data.get('company_website')  # from Attio

BASE_URL = "https://api.pitchbook.com"

# choose website if available, else name
search_query = company_website if company_website else company_name

headers = {
    "Authorization": f"PB-Token {api_key}",
    "Accept": "application/json"
}

# -------------------------
# Top investor set (canonical = CRM naming)
# Populate with your firm's watchlist, or leave empty to skip matching.
# -------------------------
TOP_INVESTORS = {
    # "Example Capital",
    # "Example Ventures",
}

# Aliases: alternate spelling/format (lowercased) → canonical name in TOP_INVESTORS
ALIASES = {
    # "example cap": "Example Capital",
}

# Case-insensitive lookup from canonical set
canonical_map = {name.lower(): name for name in TOP_INVESTORS}

# -------------------------
# Helper: safe GET (returns {} on any failure)
# -------------------------
def safe_get(url):
    try:
        resp = requests.get(url, headers=headers, timeout=15)
        if resp.ok:
            return resp.json() if resp.text else {}
        return {}
    except Exception:
        return {}

# -------------------------
# Helper: enforce USD-only
# -------------------------
def assert_usd(money_obj, field_name):
    if not money_obj:
        return
    currency = money_obj.get("currency") or money_obj.get("nativeCurrency")
    if currency and currency != "USD":
        raise Exception(f"Non-USD currency detected for {field_name}: {currency}")

# -------------------------
# Empty/no-data output shape (every field always present)
# -------------------------
def no_data_output(pb_id=None):
    return {
        "data_status": "no data",
        "pitchbook_company_id": pb_id,
        "search_used": search_query,
        "investors": "",
        "matched_investors": "None",
        "match_count": 0,
        "company_geography": "",
        "last_round_type": None,
        "total_funding_amount": None,
        "last_funding_amount": None,
        "last_funding_valuation": None,
        "last_funding_date": None
    }

# -------------------------
# 0. SEARCH → GET PB ID (graceful)
# -------------------------
if not search_query:
    return no_data_output()

search_resp = safe_get(f"{BASE_URL}/search?query={search_query}")
items = search_resp.get("items", [])

if not items:
    return no_data_output()

pb_id = None
for item in items:
    if item.get("primaryFirmType", {}).get("type") == "COMPANY":
        pb_id = item.get("pbId")
        break

if not pb_id:
    pb_id = items[0].get("pbId")

if not pb_id:
    return no_data_output()

# -------------------------
# 1. INVESTORS
# -------------------------
investors_resp = safe_get(f"{BASE_URL}/companies/{pb_id}/investors")

investors_array = investors_resp.get("investors", [])
if not isinstance(investors_array, list):
    investors_array = []

seen = set()
investor_names = []

for inv in investors_array:
    name = inv.get("investorName")
    if name and name not in seen:
        seen.add(name)
        investor_names.append(name)

investors_string = ", ".join(investor_names)

# -------------------------
# 2. MATCH TOP INVESTORS
# -------------------------
matched = []
matched_seen = set()

for name in investor_names:
    lower = name.lower()
    canonical = ALIASES.get(lower) or canonical_map.get(lower)
    if canonical and canonical not in matched_seen:
        matched.append(canonical)
        matched_seen.add(canonical)

matched_investors = ", ".join(matched) if matched else "None"
match_count = len(matched)

# -------------------------
# 3. COMPANY GEO (BIO)
# -------------------------
bio_resp = safe_get(f"{BASE_URL}/companies/{pb_id}/bio")

hq = bio_resp.get("hqLocation", {})

city = hq.get("city")
state = hq.get("stateProvince")
country = hq.get("country")

geo_parts = [p for p in [city, state, country] if p]
company_geography = ", ".join(geo_parts)

# -------------------------
# 4. MOST RECENT FINANCING
# -------------------------
fin_resp = safe_get(f"{BASE_URL}/companies/{pb_id}/most-recent-financing")

deal_type = fin_resp.get("lastFinancingDealType", {})
last_round_type = deal_type.get("description")

# -------------------------
# 5. FUNDING FIELDS (+ USD enforcement)
# -------------------------
assert_usd(bio_resp.get("totalMoneyRaised"), "totalMoneyRaised")
assert_usd(fin_resp.get("lastFinancingSize"), "lastFinancingSize")
assert_usd(fin_resp.get("lastFinancingValuation"), "lastFinancingValuation")
assert_usd(fin_resp.get("lastKnownValuation"), "lastKnownValuation")

total_funding_amount = (bio_resp.get("totalMoneyRaised") or {}).get("amount")
last_funding_amount = (fin_resp.get("lastFinancingSize") or {}).get("amount")
last_funding_date = fin_resp.get("lastFinancingDate")

last_funding_valuation = (
    (fin_resp.get("lastFinancingValuation") or {}).get("amount")
    or (fin_resp.get("lastKnownValuation") or {}).get("amount")
)

# -------------------------
# OUTPUT
# -------------------------
return {
    "data_status": "data exists",
    "pitchbook_company_id": pb_id,
    "search_used": search_query,
    "investors": investors_string,
    "matched_investors": matched_investors,
    "match_count": match_count,
    "company_geography": company_geography,
    "last_round_type": last_round_type,
    "total_funding_amount": total_funding_amount,
    "last_funding_amount": last_funding_amount,
    "last_funding_valuation": last_funding_valuation,
    "last_funding_date": last_funding_date
}