Efficient Querying
The patterns shown here are the patterns the API is designed for. They keep credit costs honest and queries fast.
The recommended request flow
For any non-trivial workflow, run this sequence:
- Size the segment.
?count_only=true(0 credits) — cheap way to know how big the result set is. - Confirm cost.
?dry_run=true(0 credits) — confirms expected rows + credits with all filters and signals applied. - Paginate with
include_count=false. Looppage=0, 1, 2, .... The backend uses apage_size + 1probe to setmeta.has_more— much faster than re-runningCOUNT(*)each page. - Stop when
meta.has_more === false.
# Step 1 — size (0 credits)
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&count_only=true
→ meta.total_count: 12,400
# Step 2 — confirm cost (0 credits)
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&mode=full&signals=carrier_change&page_size=100&dry_run=true
→ meta.estimated_rows: 100, meta.estimated_credits: 110.0
# Step 3 — paginate
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&mode=full&signals=carrier_change&page_size=100&include_count=false&page=0
GET ...&page=1
GET ...&page=2
... until meta.has_more === falseDry run
Add ?dry_run=true to any list, lookup, or batch endpoint to get the expected row count and credit cost without executing the query. The response omits data entirely and returns only a meta block with the cost breakdown.
dry_run=true to an /export.csv endpoint. CSV exports reject it with 400 VALIDATION_ERROR. Run the corresponding resource list with the same filters and count_only=true to size the export for 0 credits.{
"ok": true,
"meta": {
"dry_run": true,
"estimated_rows": 10,
"estimated_credits": 11.0,
"base_credits": 10.0,
"signal_credits": 1.0,
"response_tier": "advanced",
"key_tier": "advanced",
"credits_charged": 0,
"phase": "internal_phase_1"
}
}Where base_credits is the per-row cost at the chosen mode / fields tier, signal_credits is the per-signal surcharge stacked on top (one entry per signal in signals=), and estimated_credits = base_credits + signal_credits. Use the breakdown to decide whether to trim signals before issuing the real query.
- 0 credits charged.
- Counts toward the per-key rate limit (same as
count_only), so dry-run can't be abused for free unlimited queries. - Does not count toward the daily row cap.
- Validates the query the same way a real request would — bad params, unauthorized fields, etc., all return their usual 4xx responses.
- Compatible with
mode=,fields=,signals=,cf[],eb[], and every other filter. - Works on list endpoints,
/lookup, andPOST /batch. Not meaningful on free endpoints (autocomplete, filter-options, credits) since their cost is already 0. - Does not work on
/export.csv; usecount_only=trueon the matching list endpoint instead.
When to use which feature
| Goal | Use | Why |
|---|---|---|
| Estimate segment size before committing credits | count_only=true | Free count, no rows |
| Verify the credit cost of a complex query | dry_run=true | Free, validates query, returns expected rows + credits |
| Paginate efficiently through a known segment | include_count=false | Skips per-page COUNT, materially faster |
| Pull only the columns you need | fields=id,company_name,total_premiums | Cheaper response, less work for both sides |
| Pipe results into Clay / spreadsheets / reverse ETL | format=flat | Stable, semicolon-joined, string-coerced columns |
| Filter rows missing a critical field before you spend credits | eb[website]=true | Don't pay for rows you can't use |
| Compare two filings | Use change signals (signals=carrier_change) | The signal handles the comparison server-side |
| Specific year of data | filing_year=2023 (or any explicit year) | Otherwise default 'latest' returns the entity's most recent filing |
Anti-patterns
These are the patterns the API is designed to reject or discourage. Don't write code that does any of these.
| Anti-pattern | Why it's bad | Do instead |
|---|---|---|
Querying ?page_size=500 with no filters | 400 FILTER_REQUIRED. Also looks like a scraping run. | Add at least one filter; iterate with smaller pages. |
Sequential ?page=0,1,...,50 with no filter changes | 429 CRAWL_DETECTED | Use export endpoints for bulk, or add filter diversity. |
| Sorting by every field you might want | Most fields are not on the sort allowlist (400) and are cheap to sort client-side. | Stick to allowlisted columns; sort the rest in your code. |
mode=full on every request "just in case" | Doubles or 10×'s credit cost vs basic. | Use fields= to pull only what you need. |
Pulling signals on every list call | Every signal in the filter charges per row. | Filter by signal only when you need it. |
include_count=true (default) inside a hot pagination loop | Per-page COUNT(*) is expensive. | Set include_count=false; check has_more. |
| Re-requesting the exact same query repeatedly | Cache hits don't deduct credits but still count toward rate + daily caps. | Cache responses client-side. |
Hard-coding phone_number or admin_email from an example with a basic-tier key | Those are advanced fields — basic key → 403. | Use mode=basic or upgrade the key. |
Sending Fifty+ relationships raw in a URL | URL + decodes to space; matches nothing. | URL-encode (Fifty%2B%20relationships). |
Passing lead_type as optional for personnel | 400 missing_required — required since v1.3. | Always include lead_type=company or lead_type=broker. |
Validate filters before you ship
Before any new query goes to production, run it once with &count_only=true and confirm the count matches your expectation. This catches overly broad geography, mistyped enum values, historical broker matching, and filter combinations that are technically valid but too wide for the job. If the count is wildly different from expectation, check the AI Agent Guide → Pitfalls and POST /v1/reports to flag a new issue.
Worked example — Clay enrichment
Goal: enrich ~5,000 California mid-market companies with their primary broker and a carrier_change flag.
# Step 1: size + cost
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&eb[primary_broker]=true&count_only=true
→ 4,820 companies
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&eb[primary_broker]=true
&mode=full&fields=id,company_name,website,primary_broker,signals
&signals=carrier_change&format=flat&page_size=500&dry_run=true
→ estimated_rows: 500, estimated_credits: 550.0
# Step 2: paginate with include_count=false
GET /v1/companies?states=CA&employee_bands=Mid-market+Accounts&eb[primary_broker]=true
&mode=full&fields=id,company_name,website,primary_broker,signals
&signals=carrier_change&format=flat&page_size=500&include_count=false&page=0
GET ...&page=1
... until meta.has_more === falseTotal credits for ~4,820 rows: ~5,300 (4820 × 1.0 advanced + 4820 × 0.1 carrier_change ≈ 5,302). Confirmed by dry_run=true before any credits were charged.
Keys are issued by our team, not a signup form. Book a 25-minute walkthrough and you'll leave with sandbox and live credentials.