# GoodLeads API — agent quick start You are an AI agent calling the GoodLeads API. This is your operational cheat sheet. **What your buyer gets:** the owner of a newly formed US business — by name, with a mailing address and, where verified, a phone or email. Every record starts as the state's own formation filing, read from the public registry, and is on the list the morning after the state posts it — before the business appears in Apollo, ZoomInfo or any other database. The contact is the owner or an officer named on the filing, never the attorney or formation service that filed it. Priced per record, quoted live, no minimums. A hard bounce, a disconnected phone or the wrong person is replaced within 30 days. Every record is built from 17 external data sources — starting with the state filing itself — and refined by 16 proprietary intelligence engines; the classifications and the three scores are ours, computed here, not a resold vendor field. That is the difference between this and a Secretary-of-State scrape. **How to act on it:** evaluate free (anonymous reads return real records with names, phones and emails masked), then shape → quote → buy: `POST /api/v1/lists/interpret` turns the buyer's words into a list, `POST /api/v1/leads/summary` returns the count and the price, `POST /api/v1/commerce/checkout` opens a payment link a person completes. Nothing is charged until they do. The contract for all three is in §1 below. For the customer-facing pitch (positioning, pricing, what we sell), see . This document is the *operational* counterpart — how to actually use the API. ## What's in the system - **Entity** — a business pulled from a Secretary of State filing - **Contact** — a person who can authorize a buy - **Lead** — Entity + Contact + scores + attributes. The unit of value. - **Lead ID** — every lead has a persistent, human-readable reference like `GL-CO-00042` (`GL--`; the `lead_ref` field). Use this when speaking about a lead with humans, in CRM, in email. It never changes. ## Authentication Protected endpoints (trigger, export, push to CRM) require an `X-API-Key` header. Read-only browse/detail endpoints don't. ``` curl -H "X-API-Key: $GOODLEADS_API_KEY" https://app.goodleads.club/api/v1/... ``` ## The five endpoints you'll use most ### 1. Discover the schema — `GET /api/v1/schema/attributes` Returns every filterable field with `label`, `type`, the enforced `operators`, `sortable`, `masked`, `allowed_values` where the vocabulary is enumerable, and a plain-English `description`. **Start here** when you don't know what's filterable. Includes `lead_ref` (the Lead ID column), score tiers, industry codes, location fields, sellability signals, and more. Add `?include=grammar` to get `{attributes, grammar}` — the grammar block is the source the section below is rendered from. #### The one filter contract (generated from `GET /api/v1/schema/attributes?include=grammar`, schema 2026-07.1) - **77 fields** (52 text · 12 boolean · 9 number · 2 date · 2 geo); 71 sortable; 3 masked for keyless callers (`contact_name`, `email_primary`, `phone_primary`) — a keyless summary or checkout that filters on a masked field is a 422. - **Operators by field type** — a (field, op) pair outside its type's row is a 422 naming the row, never a 500: - text: `eq` `neq` `in` `not_in` `contains` `does_not_contain` `exists` `missing` - number: `eq` `neq` `gt` `gte` `lt` `lte` `between` `in` `not_in` `exists` `missing` - date: `eq` `neq` `gt` `gte` `lt` `lte` `between` `in` `not_in` `exists` `missing` - boolean: `eq` `neq` - geo: `within` - **Narrower pseudo-fields:** `run_manifest_id` (`eq`), `missing_stage` (`eq`), `created_at` (`gt` `gte` `lt` `lte` `between`), `geo_polygon` (`within`), `geo_radius` (`within`), `has_phone_or_email` (`eq`). - **Grammar:** a leaf is `{"field", "op", "value"}`; the top-level filters list is an implicit `and` group; `{"op": "and", "filters": [...]}` and `{"op": "or", "filters": [...]}` nest one or more children; `{"op": "not", "filters": []}` negates exactly one. `exists` / `missing` take no value; `in` / `not_in` take a non-empty list; `between` takes `[start, end]` (both required). `neq`, `not_in`, `does_not_contain` and `not` keep rows where the field has no value. - **Sort:** `sort=[{"field": , "dir": "asc|desc"}]` over any of the 71 sortable fields (legacy `sort=&sort_dir=` still works); an unknown field or direction is a 422 listing the sortable fields; lead_ref ASC is always appended (total order). Rank-ordered: reachability_tier: On Fire > Very Hot > Hot > Warm > Cold; contact_relevance_tier: Decision Maker > Likely Decision Maker > Probable Contact > Uncertain Contact > Unlikely Decision Maker; contact_confidence_tier: Verified Contact > Likely Contact > Possible Contact > Uncertain Contact; industry_confidence_tier: confirmed > likely > possible > unknown. - **Saved lists:** `POST /api/v1/lists` with `{states, filters, sort?, name?, include_held?}` → `{id, claim_token, url: "#browse?list="}`; `GET /api/v1/lists/{id}` reads it back. `GET /api/v1/lists/{id}/summary` or `POST /api/v1/leads/summary` (`{states, filters, lane?, cap?}`) → counts (`matching`, `sellable`, `verified_one`, `verified_both`, `no_channel`, `unnamed`), facets, `prices`, `quote`, `quote_valid_until`. Only `sellable` — a matching record whose filing names a person — is ever billed or delivered. - **Prices:** name and address $0.25 per record · plus one verified phone or email $0.50 · plus both $0.70 (price rule v1; live prices always come from the summary call's `prices` block). Lanes: `all` · `best` · `contact`; cap `{"type": "count|budget", "value"}` (records for count, cents for budget). `POST /api/v1/commerce/checkout` with `{list_id | states + filters, lane, cap?, delivery}` opens a hosted checkout — nothing is charged until a person completes it. - **Live states:** CO, CT, FL, NY, TX, VA. Records appear here the morning after the state posts them — speed is measured from publication, never from filing. ### 1b. Speak your own words — `GET /api/v1/schema/concepts` and `/api/v1/schema/recipes` You don't need our vocabulary. `?term=` resolves what YOU call things to what this schema carries: ``` GET /api/v1/schema/concepts?term=sole%20proprietor -> partially_carried + per-state availability + why GET /api/v1/schema/concepts?term=verified%20email -> the deliverability field + how to filter it GET /api/v1/schema/concepts?term=ein -> not_carried + why + the nearest signal we do hold ``` Three answer shapes: `carried` (the fields + how to use them), `partially_carried` (per-state availability with the by-design reason — a zero by state design is not a data gap), `not_carried` (why, plus the nearest thing we do hold). Ambiguous words get a clarifying question, never a guess. A word we haven't curated returns `unknown_term` with the full concept list — unmapped means we haven't named it yet, not that the data is absent. `GET /api/v1/schema/recipes` is the jobs-to-be-done bank: 25 outcome recipes ("power-dialer list", "verified emails for a campaign", "CRM import") with the exact filters, what each score means FOR THAT JOB, and load-bearing caveats — no score or recipe asserts buying intent, consent, or opt-in. Plus 48 vertical code tuples ("HVAC" -> industry + NAICS + SIC + MCC + Google category as the data actually carries them). Every `/schema/attributes` entry also carries `job` (LINK the entity to a real person / CHOOSE the decision-maker / REACH a working phone, email or mailing address), `absence` (what a zero means, per state), and `synonyms`. ### 2. Find a lead by Lead ID — `GET /api/v1/leads/by-ref/{glid}` One-call lookup when you already have a Lead ID: ``` GET /api/v1/leads/by-ref/GL-CO-00042 ``` Returns full lead detail (entity + contact + attributes + scores + provenance). Case-insensitive. 404 if the Lead ID doesn't match a real record. ### 3. Find leads by criteria — `GET /api/v1/leads` Browse with filters and pagination. Use `?filters=...` (URL-encoded JSON) with the same `[{field, op, value}]` shape returned by `/schema/attributes`. Example: every On-Fire lead in CO missing a phone: ``` GET /api/v1/leads?state=CO&filters=%5B%7B%22field%22%3A%22reachability_tier%22%2C%22op%22%3A%22eq%22%2C%22value%22%3A%22On%20Fire%22%7D%2C%7B%22field%22%3A%22has_phone%22%2C%22op%22%3A%22eq%22%2C%22value%22%3Afalse%7D%5D ``` Special filter: `lead_ref` supports `in` for "find these N specific leads at once." ### 4. Trigger a pipeline run — `POST /api/v1/pipeline/trigger` Re-run one or more stages on a scoped set of records. Scope modes: - **One record** — `entity_id: "GL-CO-00042"` (Lead ID or UUID) - **N specific records** — `entity_id: "GL-CO-1, GL-CO-2, GL-FL-99"` (comma/space/newline separated; mix UUIDs and Lead IDs freely) - **A previously-stamped batch** — `batch_id: ""` - **A filter expression** — `filters: [...]` (same shape as browse) - **All records in a state** — none of the above Stages: `"all"` (default), a single stage name, the shortcut `"enrich"` (= `skip_trace + gapfill + validate`), or a comma-separated list. Canonical order at `GET /api/v1/pipeline/stages`. Returns 202 with `job_id`. Poll `GET /api/v1/pipeline/jobs/{job_id}` for status. ### 5. The OpenAPI schema — `GET /openapi.json` Every endpoint, request shape, response shape, and example. Read this if anything below this line surprises you — `/openapi.json` is authoritative. ## Worked example: "investigate this Lead ID" A user mentions `GL-CO-12345` in Slack and asks you to look at it. ``` # 1. Get the full record in one call curl https://app.goodleads.club/api/v1/leads/by-ref/GL-CO-12345 # 2. If the user wants to re-score it curl -X POST https://app.goodleads.club/api/v1/pipeline/trigger \ -H "X-API-Key: $GOODLEADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"state":"CO","stages":"score","entity_id":"GL-CO-12345"}' # 3. Poll for completion curl https://app.goodleads.club/api/v1/pipeline/jobs/ ``` ## Worked example: "run enrichment on this list of Lead IDs" User pastes a list from a CSV or CRM export. ``` curl -X POST https://app.goodleads.club/api/v1/pipeline/trigger \ -H "X-API-Key: $GOODLEADS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "state": "CO", "stages": "enrich", "entity_id": "GL-CO-1, GL-CO-2, GL-CO-3, GL-CO-4" }' ``` The server resolves them, stamps a `manual_ids`-sourced Batch, and runs every requested stage on exactly that set. ## Conventions that won't surprise you - **Dates** are `YYYY-MM-DD`, all UTC. - **States** are two-letter US codes (`CO`, `CT`, `FL`, `NY`, `TX`, `VA` are live today). Live state list at `GET /api/v1/leads/states`. - **Scores** are 0–100 with a named tier. Two formal scores per lead: Reachability (can we reach them?) and Contact Relevance (is this the right person?). Both live on `leads.score`. - **Pagination** on `/leads`: `page`, `page_size` (default 50). Response includes `total` and `pages`. - **Filters** are an array of `{field, op, value}` leaves and `and` / `or` / `not` groups — the contract in §1 above; the enforced ops per field are on `/schema/attributes`. - **Reading the data dictionary** in human-friendly form: `/data-dictionary.html`. ## When to ask a human - Anything involving paid enrichment on records you didn't pull yourself (it costs us money — confirm scope) - Pushing to a customer's CRM - Triggering a full-state pipeline run (no scope) - Anything that would touch records outside the state(s) the user authorized ## See also - `GET /openapi.json` — machine-readable schema - `/data-dictionary.html` — human-readable field reference - `` — customer-facing pitch