Hillwinds API
Core Concepts

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:

  1. Size the segment. ?count_only=true (0 credits) — cheap way to know how big the result set is.
  2. Confirm cost. ?dry_run=true (0 credits) — confirms expected rows + credits with all filters and signals applied.
  3. Paginate with include_count=false. Loop page=0, 1, 2, .... The backend uses a page_size + 1 probe to set meta.has_more — much faster than re-running COUNT(*) each page.
  4. Stop when meta.has_more === false.
bash
# 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 === false

Dry 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.

CSV exports do not support dry_run
Do not send 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.
json
{
  "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, and POST /batch. Not meaningful on free endpoints (autocomplete, filter-options, credits) since their cost is already 0.
  • Does not work on /export.csv; use count_only=true on the matching list endpoint instead.

When to use which feature

GoalUseWhy
Estimate segment size before committing creditscount_only=trueFree count, no rows
Verify the credit cost of a complex querydry_run=trueFree, validates query, returns expected rows + credits
Paginate efficiently through a known segmentinclude_count=falseSkips per-page COUNT, materially faster
Pull only the columns you needfields=id,company_name,total_premiumsCheaper response, less work for both sides
Pipe results into Clay / spreadsheets / reverse ETLformat=flatStable, semicolon-joined, string-coerced columns
Filter rows missing a critical field before you spend creditseb[website]=trueDon't pay for rows you can't use
Compare two filingsUse change signals (signals=carrier_change)The signal handles the comparison server-side
Specific year of datafiling_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-patternWhy it's badDo instead
Querying ?page_size=500 with no filters400 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 changes429 CRAWL_DETECTEDUse export endpoints for bulk, or add filter diversity.
Sorting by every field you might wantMost 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 callEvery signal in the filter charges per row.Filter by signal only when you need it.
include_count=true (default) inside a hot pagination loopPer-page COUNT(*) is expensive.Set include_count=false; check has_more.
Re-requesting the exact same query repeatedlyCache 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 keyThose are advanced fields — basic key → 403.Use mode=basic or upgrade the key.
Sending Fifty+ relationships raw in a URLURL + decodes to space; matches nothing.URL-encode (Fifty%2B%20relationships).
Passing lead_type as optional for personnel400 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.

bash
# 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 === false

Total 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.

Cross-references
See also Pagination & Sorting for sort allowlists, Credits & Usage for full pricing examples, and Signals for per-signal cost details.
Need a key?

Keys are issued by our team, not a signup form. Book a 25-minute walkthrough and you'll leave with sandbox and live credentials.