TL;DR
ZoomInfo exposes licensed B2B data through several APIs. Search, Enrich, Lookup, Usage, Bulk, Webhooks, WebSights, and Compliance return JSON, but your contract determines which endpoints and fields you can use. Current apps use OAuth 2.0, while some legacy integrations still use PKI-generated JWTs. Public-web extraction is a separate option for facts already published on company websites. It cannot access or copy ZoomInfo records.
The companion Google Colab contains the tested public-web workflow, its July 28 output, and the CSV export.
What is the ZoomInfo API?
The ZoomInfo API gives entitled customers programmatic access to ZoomInfo's proprietary company, contact, intent, and related B2B datasets. Depending on the contract, an integration can search the database, enrich a known company or person, inspect usage, run bulk jobs, receive change notifications, identify website traffic at company level, or process privacy requests. Responses are JSON.
ZoomInfo does not offer this as a free public API. Your organization needs API access, the relevant endpoint and field entitlements, and credentials issued for its contract. A normal platform login is not enough. Missing access or field entitlements explain many 403 errors and much of the confusion around ZoomInfo API pricing.
Request fields and endpoint behavior can change, so use the ZoomInfo API documentation as the authority. Select the API family based on the job, use the credentials provisioned for the account, monitor both rate and contract limits, and check whether the needed facts already exist on public company websites.
Source: ZoomInfo API documentation overview, captured July 28, 2026.
Which ZoomInfo API does what?
ZoomInfo separates its interfaces by task instead of placing every operation behind one generic endpoint.
| API | Main job | Use it when |
|---|---|---|
| Search | Find matching licensed records | You are starting with filters |
| Enrich | Complete a record you know | You have a company domain or identifying details |
| Lookup | Discover supported fields | You are building a Search or Enrich request |
| Usage | Track allowance consumption | You need request and record usage data |
| Bulk | Run asynchronous retrieval jobs | Synchronous pagination is too small |
| Webhooks | Receive record changes | ZoomInfo should notify your endpoint |
| WebSights | Resolve site traffic | You have visitor IP context |
| Compliance | Support privacy operations | You are processing opt-outs or related requests |
Search API
Use Search to discover records from filters. If you already know the company and want to fill gaps in its record, use Enrich instead. The documentation lists contacts, companies, scoops, news, and intent among the searchable record types. A sales operations team could search for companies in a market segment and request only the entitled fields it needs.
Search returns at most 100 results per page, and pagination stops after 1,000 total records. If a request needs more, submit a Bulk job instead of trying to page past the cap.
Enrich API
The ZoomInfo Enrich API starts with a record you already have and attempts to match it to a ZoomInfo profile. A company domain is the usual company key. Contact enrichment can use the identifying details allowed by the endpoint. You must request output fields; omitting the required outputFields parameter causes an error.
Search asks, "which records satisfy these filters?" Enrich asks, "what can ZoomInfo add to this known record?" They use different request shapes and may consume the contract differently. A lead-generation scraping workflow can include both, along with public-site research, but those stages remain separate even when their final rows land in the same CRM.
Lookup and Usage APIs
Call Lookup before hard-coding input or output fields. A field shown in an example may still be unavailable for your endpoint, record type, or subscription.
Usage shows how much of the contract allowance remains. ZoomInfo exposes it through a lookup endpoint and response headers. Read those headers in production. A successful development test says nothing about whether a nightly job will exhaust its record allowance halfway through the month.
Bulk and Webhooks
Bulk handles asynchronous company and contact search or retrieval beyond the synchronous pagination limit. Submit the work, store the returned identifier, poll at a reasonable interval, and download the result after the job completes.
Webhooks remove the need to poll repeatedly for record changes. Register a supported subscription and let ZoomInfo notify your HTTPS endpoint. The receiver should follow the current verification rules, process events idempotently, store an event identifier, return quickly, and send heavier work to a queue.
WebSights and Compliance
WebSights handles company-level website visitor identification. It resolves IP context to company, ISP, and geolocation information when ZoomInfo has a match. It is not an individual visitor lookup, and an IP does not always map to a company. Build attribution logic around the fields and confidence in the actual response.
Compliance endpoints support privacy and opt-out workflows for contacts. Include them in any application that stores licensed person data. Treat a deletion or opt-out signal as operational data that requires prompt processing.
Access, credentials, and authentication
"ZoomInfo API key" is a common search phrase, but ZoomInfo uses OAuth 2.0 for new apps. A server-to-server integration exchanges its client ID and client secret for a bearer token through the Client Credentials flow. An application acting for a signed-in user uses the Authorization Code flow with PKCE. In both cases, the ZoomInfo app controls scopes and attribution.
The legacy API documentation uses PKI instead. Its Python, Java, and Node.js libraries generate a one-hour JWT from a client ID and private key. Existing integrations may still have those credentials, but a new integration should use the flow provisioned in its ZoomInfo app unless ZoomInfo tells the account owner otherwise.
Set up authentication in this order:
- Confirm that the contract includes API access and the specific API families you need.
- Create or obtain the ZoomInfo app, scopes, and integration user required by the account.
- Store the client ID and client secret, or legacy private key, in a secret manager.
- Request and cache the bearer token, then refresh it according to the returned
expires_invalue.
A bearer token proves who the caller is. It does not override the subscription. You can authenticate successfully and still receive a 403 because Search, Enrich, a particular output field, or the remaining usage allowance is not available to the account.
Python client-credentials example
This Python request mirrors ZoomInfo's Client Credentials example. It is source-verified, not executed here without ZoomInfo entitlement and a provisioned app.
import os
import requests
client_id = os.environ["ZOOMINFO_CLIENT_ID"]
client_secret = os.environ["ZOOMINFO_CLIENT_SECRET"]
response = requests.post(
"https://api.zoominfo.com/gtm/oauth/v1/token",
auth=(client_id, client_secret),
data={"grant_type": "client_credentials"},
headers={"Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
token = response.json()
access_token = token["access_token"]
expires_in = token["expires_in"]
# Cache access_token until shortly before expires_in.
# Never print or log the token or client secret.Keep token retrieval in one small authentication module. Callers should ask that module for a valid token rather than requesting their own. Add a lock around refresh in concurrent applications so ten workers do not all request a token at once.
If your account still uses the legacy PKI flow, keep its client ID and private key in the same class of secret storage. Do not switch credential types until the account owner confirms the migration path. User-facing applications should use Authorization Code with PKCE instead of collecting a ZoomInfo username and password.
Why the ZoomInfo API returns 403
A 403 is usually an entitlement or allowance problem, not malformed JSON. The official error list distinguishes several cases:
- the subscription does not allow one or more requested fields;
- the account is not provisioned for the Enrich endpoint;
- the account is not provisioned for the Search endpoint;
- the account lacks access to the requested endpoint;
- the contract's request limit has been used;
- the contract's record limit has been used;
- WebSights has exhausted its own request or record allowance.
Log the HTTP status, ZoomInfo error message, request ID if present, and the rate and usage headers. Do not log bearer tokens, client secrets, legacy private keys, or the full response when it contains licensed personal data.
Start by calling Lookup or comparing the requested fields with the current docs. Then inspect the usage headers and confirm endpoint provisioning with the account administrator. Before contacting ZoomInfo, record the failing endpoint, field set, and allowance. "The API gives 403" does not give support enough information to diagnose the request.
Other status codes narrow the problem. A 400 can mean malformed JSON, invalid fields, a missing outputFields, bad pagination, or insufficient matching input. A 401 means authentication failed. A 429 means the request rate is too high. A 500 can be a server problem or a query too broad to complete reliably.
ZoomInfo API rate limits and usage limits
ZoomInfo publishes per-second, per-hour, and per-day limits by package:
| Package | Per second | Per hour | Per day |
|---|---|---|---|
| Builder | 5 | 10,800 | 129,600 |
| Standard | 25 | 54,000 | 648,000 |
| Scaling | 35 | 75,600 | 907,200 |
Source: ZoomInfo Rate Limits, captured July 28, 2026.
Treat the table as enforced ceilings and leave headroom below them. Read the X-RateLimit-Limit-* and X-RateLimit-Remaining-* headers for the second, hour, and day windows. After a 429 response, inspect Retry-After, X-RateLimit-Rejected-Bucket, and X-RateLimit-Reset before retrying. The older X-RateLimit-Limit and X-RateLimit-Remaining headers are deprecated and scheduled for removal on January 1, 2027.
Contract usage is separate. ZoomInfo can limit total requests and records across the contract term. Response headers expose the remaining request, record, unique-ID, and WebSights allowances where relevant. A client can stay below 25 requests per second and still fail after consuming its record allowance.
For a batch integration, estimate both dimensions before launch:
daily requests = input rows / records matched per request
daily records = sum of records returned or enriched
peak rate = active workers x requests per worker per secondLeave headroom for retries, manual investigations, and end-of-period jobs. Use Bulk for batch work and Usage for monitoring. A 403 should not be the first signal that an allowance is nearly empty.
Pricing and cost
ZoomInfo quotes API access as part of a contract. It does not publish a self-serve API price. The ZoomInfo pricing page sends buyers to sales, while the API documentation describes request and record allowances tied to the contract term.
A quote is difficult to evaluate unless it names the included products, endpoint entitlements, output fields, and expected request or record volume. Bulk, WebSights, intent data, and company enrichment are not interchangeable units.
Ask for these items in writing before signing:
- which API families and environments are provisioned;
- which company and contact fields are included;
- request, record, unique-ID, and WebSights allowances;
- whether unused allowances expire and what happens at the limit;
- Bulk and webhook availability;
- overage, renewal, support, and data-retention terms;
- a usage export that finance and engineering can reconcile.
Compare the quote with the value of ZoomInfo's licensed dataset, not with the cost of fetching a public page. Similarweb API pricing has the same split: proprietary data and public-web extraction solve different problems even when both return JSON.
ZoomInfo data vs. public-web extraction
Search results sometimes call a public-web workflow a "ZoomInfo scraper API." That wording hides the data boundary. ScrapeGraphAI does not provide access to ZoomInfo's authenticated product, licensed records, contact database, or protected endpoints. It extracts information from public URLs that you choose.
| Requirement | ZoomInfo API | Public-web extraction |
|---|---|---|
| Licensed B2B company and contact profiles | Native fit | Not available unless the facts are public on the source site |
| Intent, scoops, and proprietary signals | Native fit | Not a substitute |
| Contract-governed enrichment at scale | Native fit | Different data source and rights model |
| Current product categories from a company site | Possible if included in licensed data | Native fit |
| Pricing and contact page URLs | May be fields in a profile | Extract directly from the public site |
| Custom fields unique to your research | Limited to available fields | Define them in your own schema |
| Self-serve testing on public pages | Contract dependent | Native fit |
Choose ZoomInfo when you need its licensed dataset. Choose public-web extraction when you need current facts from a public company website in a custom schema. One integration can use both: ZoomInfo for licensed discovery and enrichment, then public websites for product, documentation, integration, or pricing facts used in its qualification model.
The web scraping API guide explains the differences among fetch APIs, browser rendering, structured extraction, and search services.
Python public-website company enrichment
The code reads only three public homepages: scrapegraphai.com, posthog.com, and airbyte.com. It never requests ZoomInfo pages or proprietary records. The schema asks for company name, description, product categories, pricing URL, contact URL, and public social URLs.
The 2.x SDK requires Python 3.12 or newer. This workflow was tested with Python 3.12 and scrapegraph-py 2.1.0, so the install is pinned for reproducibility:
python -m pip install -q "scrapegraph-py==2.1.0" "pydantic>=2,<3" pandasPut the ScrapeGraphAI key in the environment instead of pasting it into the script:
export SGAI_API_KEY="replace-with-your-key"Define and validate the company schema
import os
from pydantic import BaseModel, Field
from scrapegraph_py import FetchConfig, ScrapeGraphAI
class CompanyProfile(BaseModel):
company_name: str = Field(min_length=1)
description: str = Field(min_length=20)
product_categories: list[str] = Field(min_length=1)
pricing_url: str | None = None
contact_url: str | None = None
public_social_urls: list[str] = Field(default_factory=list)
PROMPT = """
Read this public company website and return only facts visible on the site.
Extract the company name, a concise description, product categories, the
absolute pricing page URL when linked, the absolute contact page URL when
linked, and public company social profile URLs linked by the site. Do not
guess missing URLs and do not return personal contact data.
""".strip()
sgai = ScrapeGraphAI(api_key=os.environ["SGAI_API_KEY"])The required name, description, and category fields cause thin responses to fail validation. Optional URLs remain None when the company does not publish or link them. CompanyProfile.model_validate keeps unvalidated API data out of the rest of the program.
Extract one company and retry once
def request_profile(url: str, use_javascript: bool = False) -> CompanyProfile:
options = {}
if use_javascript:
options["fetch_config"] = FetchConfig(mode="js", wait=2000)
response = sgai.extract(
PROMPT,
url=url,
schema=CompanyProfile.model_json_schema(),
**options,
)
if response.status != "success":
raise RuntimeError(f"extract returned status={response.status}")
return CompanyProfile.model_validate(response.data.json_data)
def enrich_public_company(url: str) -> CompanyProfile:
try:
return request_profile(url)
except Exception as first_error:
print(f"[retry] {url}: {first_error}")
try:
return request_profile(url, use_javascript=True)
except Exception as retry_error:
raise RuntimeError(
f"{url} failed with the default fetch and one JavaScript retry"
) from retry_error
company = enrich_public_company("https://scrapegraphai.com")
print(company.model_dump_json(indent=2))The code retries once, only after a failed request or a response that does not satisfy the required schema. Healthy pages skip browser rendering. Store both failure messages in production so you can distinguish a fetch problem from a validation problem.
Enrich three companies and export CSV
import json
import pandas as pd
TARGETS = {
"scrapegraphai.com": "https://scrapegraphai.com",
"posthog.com": "https://posthog.com",
"airbyte.com": "https://airbyte.com",
}
profiles: list[CompanyProfile] = []
errors: dict[str, str] = {}
for domain, url in TARGETS.items():
try:
profile = enrich_public_company(url)
profiles.append(profile)
print(f"[ok] {domain}: {profile.company_name}")
except Exception as error:
errors[domain] = str(error)
print(f"[error] {domain}: {error}")
if errors:
joined = "; ".join(f"{domain}: {error}" for domain, error in errors.items())
raise RuntimeError(f"company enrichment incomplete: {joined}")
rows = []
for profile in profiles:
row = profile.model_dump(mode="json")
row["product_categories"] = json.dumps(row["product_categories"])
row["public_social_urls"] = json.dumps(row["public_social_urls"])
rows.append(row)
frame = pd.DataFrame(rows)
frame.to_csv("company_enrichment.csv", index=False)
assert len(frame) == len(TARGETS)
assert frame["company_name"].str.len().gt(0).all()
assert frame["description"].str.len().ge(20).all()
assert frame["product_categories"].ne("[]").all()
print(f"wrote company_enrichment.csv with {len(frame)} validated rows")Live output from July 28, 2026:
[ok] scrapegraphai.com: ScrapeGraphAI
[ok] posthog.com: PostHog
[ok] airbyte.com: Airbyte
wrote company_enrichment.csv with 3 validated rowsThe validated rows contained these public URLs and selected categories:
| Company | Selected product categories | Pricing URL | Contact URL |
|---|---|---|---|
| ScrapeGraphAI | Web Scraping, API, Data Extraction, Web Monitoring | Pricing | Contact |
| PostHog | Product Analytics, Session Replay, Feature Flags, Experiments | Pricing | Contact |
| Airbyte | Data replication, AI agents, Data integration platform | Pricing | Contact |
The script writes the CSV only after all three sites pass. If one site fails twice, it reports the domain and stops before writing the file. A production queue can store failures separately and retry them later, but it should never turn a failed row into an empty successful row.
The ScrapeGraphAI Python SDK guide documents this pattern: generate JSON Schema with model_json_schema(), check the response status, and validate the returned JSON with model_validate().
Production checks
Company websites change without telling your pipeline. Record the source URL and retrieval time beside each row, validate URLs before downstream use, and compare material fields with the previous successful snapshot.
A homepage might publish a slogan, a product definition, and an SEO description that disagree slightly. Ask for a concise visible description, preserve the source URL, and avoid turning marketing copy into an unsupported legal or financial claim.
Product categories drift too. Normalize obvious spelling variants after extraction, not inside the prompt. Keep the raw validated categories and a separate mapped taxonomy so an update to your taxonomy does not erase what the source actually said.
Cap concurrency, set timeouts, and track the cost per successful company. Retrying every page with JavaScript can cost more than using browser rendering only after validation fails.
Privacy, robots rules, and data boundaries
Public availability is not blanket permission for every use. Check the target site's terms, robots directives, applicable law, and your reason for processing the data. Rate-limit requests, identify your crawler where appropriate, and honor removal or opt-out requirements. Our longer guide on whether web scraping is legal covers the questions to review with counsel.
This workflow intentionally avoids personal emails, phone numbers, authenticated pages, access controls, and ZoomInfo records. It does not bypass a paywall, generate ZoomInfo credentials, evade API entitlements, or recreate proprietary intent signals. It reads three public company sites and asks for company-level facts those sites publish themselves.
For licensed ZoomInfo data, use the official API under your agreement and send privacy workflows through the Compliance API. For public-web data, document the source and collection basis. Mixing the two in one warehouse does not make their rights, retention rules, or deletion obligations identical.
Common ZoomInfo API questions
Does ZoomInfo have an API?
Yes. ZoomInfo documents Enterprise Search, Enrich, Lookup, and Usage APIs, Scaling APIs for Bulk and Webhooks, WebSights, and Compliance endpoints. Access is provisioned under a ZoomInfo contract.
How do I get ZoomInfo API access?
Confirm the required API family and fields with ZoomInfo, purchase the appropriate entitlement, and have credentials provisioned. A normal platform login is not evidence that the API or a specific endpoint is enabled.
Is there a ZoomInfo API key?
Developers often use that phrase, but new ZoomInfo apps use OAuth 2.0 client credentials or Authorization Code with PKCE. Legacy accounts may still have a PKI client ID and private key that generate a one-hour JWT. Use the credential flow provisioned for your ZoomInfo app and account.
What does ZoomInfo API enrichment do?
It matches known company or contact input to a ZoomInfo profile and returns the output fields allowed by your subscription. It differs from Search, which discovers lists of records from filters.
How much does the ZoomInfo API cost?
ZoomInfo does not publish a single self-serve API price. Cost depends on the contract, products, endpoint and field entitlements, and request or record allowances. Get those limits and overage terms in the quote.
Can a scraper replace the ZoomInfo Enrich API?
Not for proprietary ZoomInfo records, licensed contact data, intent, or scoops. Public-web extraction can fill a different need: structured facts that companies publish on their own websites, in a schema you control.

