CoverFi API Reference
Load this file into Cursor, Claude Code, Codex, or any LLM when integrating CoverFi. Human-readable docs: https://coverfi.com/docs Create API keys: https://coverfi.com/api-keys (sign-in required)
Base URL: https://coverfi.com/api Auth: Authorization: Bearer sk_live_<64 hex chars> Content-Type: application/json (for POST bodies) Errors: RFC 9457 application/problem+json
Quick start
- Sign in at https://coverfi.com and open API Keys.
- Create a key (name + FCRA permitted-use attestation). Copy the
sk_live_…value — it is shown once. - Note your organization id (
org_id) from the dashboard URL or org settings. - Call records endpoints with the key:
curl -sS "https://coverfi.com/api/orgs/$ORG_ID/records?q=Jane%20Doe&state=CA" \
-H "Authorization: Bearer sk_live_YOUR_KEY"
Prefer clicking over curl? Try every endpoint live from your dashboard in the API tester — login required; paid endpoints use real credits.
The two-step flow
The records API is a two-step funnel — the same two calls whether or not the org has credits; only the masking changes:
- Search (
GET /orgs/{org_id}/records) returns a list of matching
people. Every row carries CoverFi's permanent GD- record ID and a resolved detail_url. The envelope next_step documents the profile retrieval cost, the free re-fetch window, and the HTTP 428 first-paid-view confirmation header (X-Confirm-Shared-Access).
- With credits: 1 credit is charged and rows come back unmasked
(contact fields stay teaser-masked at the list level — their values come from the profile).
- Without credits: the search still returns
200with the same rows,
but echo-only masked — see Masking semantics.
- Profile (
GET /orgs/{org_id}/records/{record_id}) drills into one
person by their GD- ID (or via the row's detail_url).
- With credits: 1 credit is charged automatically and the profile
is returned. Re-fetching the same record within 10 minutes is free.
- Without credits: the endpoint degrades instead of erroring —
200 with a masked profile, masked: true, and an unmask hint. No charge, no error handling needed.
Check the top-level masked flag before treating any response as complete.
Python
import httpx
API = "https://coverfi.com/api"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
with httpx.Client(base_url=API, headers=HEADERS) as client:
# Step 1: search — returns a list with permanent GD- record ids.
search = client.get(
f"/orgs/{ORG_ID}/records", params={"q": "Jane Doe", "state": "CA"}
).raise_for_status()
page = search.json()
print(search.headers["X-Credits-Balance"]) # balance after the call
for row in page["items"]:
# Step 2: profile by GD- id. Auto-charges 1 credit when funded;
# returns a free masked profile (HTTP 200) when not.
profile = client.get(f"/orgs/{ORG_ID}/records/{row['id']}").raise_for_status().json()
if profile["masked"]:
print("masked — unmask at", profile["unmask"]["purchase_url"])
else:
print(profile["full_name"], profile["phones"])
TypeScript
const API = "https://coverfi.com/api";
const headers = { Authorization: `Bearer ${process.env.COVERFI_API_KEY}` };
// Step 1: search
const search = await fetch(
`${API}/orgs/${orgId}/records?q=Jane%20Doe&state=CA`, { headers },
);
const page = await search.json();
console.log(search.headers.get("X-Credits-Balance")); // balance after the call
// Step 2: profile by the row's permanent GD- id
const profile = await (
await fetch(`${API}/orgs/${orgId}/records/${page.items[0].id}`, { headers })
).json();
if (profile.masked) {
console.log("masked — unmask at", profile.unmask.purchase_url);
} else {
console.log(profile.full_name, profile.phones);
}
Authentication
API keys (programmatic)
| Header | Value |
|---|---|
Authorization | Bearer sk_live_… |
- Keys are org-scoped. The
{org_id}in the path must match the key’s organization. - Keys authenticate as member level. They can call records endpoints; they cannot create/revoke keys, manage billing, or change members (use a browser session for those).
- Prefix format:
sk_live_+ 64 hex characters. Only the prefix is shown after creation; the full secret is shown once. - Revoked keys return
401.
Session JWT (dashboard / admin)
Browser sessions use a short-lived JWT (Bearer <jwt>). Same org-scoped paths. Prefer API keys for server-to-server integrations.
Managing keys (JWT only)
| Method | Path | Role |
|---|---|---|
POST | /orgs/{org_id}/api-keys | Admin |
GET | /orgs/{org_id}/api-keys | Member |
DELETE | /orgs/{org_id}/api-keys/{key_id} | Admin |
Create body:
{
"name": "Production",
"permitted_use_accepted": true
}
Create response includes raw_key (once) plus metadata (id, name, key_prefix, created_at).
Credits
Every paid records call reserves credits from the org wallet, then captures on success / releases on failure.
| Action | Credits | Endpoints |
|---|---|---|
| Record search | 1 | GET …/records |
| Record profile (detail) | 1 | GET …/records/{record_id} |
| People at address | 2 | GET …/records/at-address |
| Identity check | 0 (free) | POST …/records/check |
| Record preview | 0 (free) | GET …/records/{record_id}/preview |
| Recent queries | 0 (free) | GET …/records/recent |
Insufficient balance on search and profile does not error — the call returns 200 with a masked response (masked: true + unmask hint; see Masking semantics). Other paid endpoints return a problem+json credit error. Buy credit packs in the dashboard.
Credit headers
Metered JSON responses (search, profile, preview, people-at-address — including the profile 428) carry the spend right in the headers, so you never need a second call to track credits:
| Header | Meaning |
|---|---|
X-Credits-Charged | Credits captured by this request (0 on masked, deduped, refunded, preview, and 428 responses) |
X-Credits-Balance | Org balance after this request |
SSE (Accept: text/event-stream) responses cannot carry them — billing finalizes after headers flush — so streaming clients read the done frame's refunded / masked flags instead.
Credit endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /credits/actions | none | Public catalog of action costs |
GET | /orgs/{org_id}/credits | API key or JWT | Wallet: {balance, reserved, monthly_allowance, allowance_period, pending_holds} |
curl -sS "https://coverfi.com/api/orgs/$ORG_ID/credits" \
-H "Authorization: Bearer $COVERFI_API_KEY"
# {"balance": 42, "reserved": 0, "monthly_allowance": 0, ...}
Rate limits
Per-IP (SlowAPI). Exceeding returns 429 with a Retry-After header; every rate-limited endpoint also emits X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on success.
| Endpoint | Limit |
|---|---|
| Search | 30/minute |
| Identity check (auth) | 20/minute |
| People at address | 30/minute |
| Record detail | 60/minute |
| Recent queries | 60/minute |
| Public free check | 1/hour |
Match-in rules (required input)
Open browsing is blocked. Requests that fail these rules return a 4xx problem with an insufficient-criteria code.
Search (GET /orgs/{org_id}/records)
At least one of:
emailphoneq(name) and (stateorzipordob)
Identity check (POST …/records/check)
full_name+date_of_birth+ (emailorphone)
People at address (GET …/records/at-address)
house_number+street_nameand (zipor (city+state))
Endpoints
All paths below are relative to https://coverfi.com/api.
Record IDs
Every record is identified by an opaque public ID starting with GD- (e.g. GD-AH4XK2M9TQV7…). These are CoverFi's own permanent identifiers — minted by our system, not keys from any upstream data source — and they are stable: the same person keeps the same ID across dataset refreshes, so they are safe to store as foreign keys in your systems. Search, identity check, and household responses return them; pass one to the record-profile and properties endpoints. Legacy record IDs issued before the GD- format remain accepted on all endpoints indefinitely, but responses only ever emit GD- IDs.
Masking semantics
Responses you have not paid for are masked, with one consistent contract:
- The response envelope carries `masked: true` and an `unmask`
object: { "credit_cost": 1, "purchase_url": "https://coverfi.com/billing", "message": "…" }.
- Masked string fields that have a value carry the literal phrase
`"Buy credits to unmask"`; fields with no value stay null.
- Masked typed fields (
age,date_of_birth,zip,gender) collapse
to null. List fields keep their counts, with each entry's values replaced by the phrase.
- `inquiry_matches` are masked whenever the search was uncharged
(blinded OR refunded-zero-result). The envelope flag for that is `inquiry_masked`, which can be true even when masked is false (refunded searches). Clients must check flags, never match the phrase.
Zero-credit search results are additionally echo-only: a field is revealed only when you searched by it, plus the person's first name and state, which always show. Search "Jane Doe" and surnames show; add city=… and cities show; everything you did not send stays masked. Even echoed contact fields never exceed the paid-list teaser fidelity (j***@example.com). Zero-credit profiles show name parts, gender, age, state, and structure (counts of phones, emails, addresses, identifiers) with all values masked.
Detect masking programmatically with the masked / inquiry_masked flags — never by matching the phrase text.
#### Two list-masking layers (masking envelope)
Search responses also carry a top-level masking object so clients can tell teaser list fidelity apart from zero-credit blinding without parsing copy:
- `masking.state: "teaser"` (charged search) — rows are otherwise
complete; email_masked / phone_masked always stay at teaser fidelity on lists. Remedy: fetch_record (use the row's detail_url / next_step).
- `masking.state: "blinded"` (zero-credit free preview) — whole rows are
echo-only. Remedy: buy_credits (fund at unmask.purchase_url and re-run).
Branch on masking.state / masking.remedy (and structured next_step fields) — never parse masking.message or next_step.message. inquiry_matches ids are inquiry UUIDs, not GD- record ids, and cannot be substituted into next_step.url_template.
Search records
GET /orgs/{org_id}/records
Query parameters
| Param | Type | Notes |
|---|---|---|
q | string | Name free text — "First Last" or "Last, First". Two-token queries without a comma try both orderings automatically. |
city | string | |
state | string | 2-letter US state |
zip | string | |
email | string | |
phone | string | |
dob | string | YYYYMM / YYYYMMDD prefix match |
age_min / age_max | int | 0–130 |
deceased | string | true / false / 1 / 0 |
limit | int | 1–25 (free plan capped at 5) |
offset | int | Pagination |
Response 200 (org with credits — 1 credit charged)
{
"items": [
{
"id": "GD-AH4XK2M9TQV7…",
"detail_url": "https://coverfi.com/api/orgs/{org}/records/GD-AH4XK2M9TQV7…",
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"gender": "F",
"age": 42,
"deceased": false,
"city": "Los Angeles",
"state": "CA",
"zip": "90001",
"email_masked": "j***@example.com",
"phone_masked": "15*******67",
"match_tier": null,
"match_confidence": null
}
],
"has_more": true,
"limit": 25,
"offset": 0,
"blinded": false,
"masked": false,
"inquiry_masked": false,
"unmask": null,
"deduped": false,
"refunded": false,
"inquiry_matches": [],
"match_type": null,
"relaxed": [],
"tiers": [],
"next_step": {
"action": "fetch_record",
"method": "GET",
"url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}",
"credit_cost": 1,
"free_refetch_minutes": 10,
"confirm_shared_access_header": "X-Confirm-Shared-Access",
"preview_url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}/preview",
"access_info_url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}/access-info",
"message": "…"
},
"masking": {
"state": "teaser",
"teaser_fields": ["email_masked", "phone_masked"],
"remedy": "fetch_record",
"message": "…"
}
}
Response 200 (org without credits — no charge, echo-only masked; the caller searched q=Jane Doe&state=CA, so the surname echoes but city, zip, age, gender, and contacts do not)
{
"items": [
{
"id": "GD-AH4XK2M9TQV7…",
"detail_url": "https://coverfi.com/api/orgs/{org}/records/GD-AH4XK2M9TQV7…",
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"gender": null,
"age": null,
"deceased": false,
"city": "Buy credits to unmask",
"state": "CA",
"zip": null,
"email_masked": "Buy credits to unmask",
"phone_masked": "Buy credits to unmask"
}
],
"has_more": true,
"limit": 25,
"offset": 0,
"blinded": true,
"masked": true,
"inquiry_masked": true,
"unmask": {
"credit_cost": 1,
"purchase_url": "https://coverfi.com/billing",
"message": "Buy credits to unmask search results."
},
"next_step": {
"action": "fetch_record",
"method": "GET",
"url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}",
"credit_cost": 1,
"free_refetch_minutes": 10,
"confirm_shared_access_header": "X-Confirm-Shared-Access",
"preview_url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}/preview",
"access_info_url_template": "https://coverfi.com/api/orgs/{org}/records/{record_id}/access-info",
"message": "…"
},
"masking": {
"state": "blinded",
"teaser_fields": ["email_masked", "phone_masked"],
"remedy": "buy_credits",
"message": "…"
}
}
Search results always mask email/phone to teaser fidelity. The contact fields a record holds come from its detail_url / the profile endpoint. refunded: true means the search found no one and the credit was returned; deduped: true means this was a free re-run of a search you already paid for.
curl -sS "https://coverfi.com/api/orgs/$ORG_ID/records?q=Jane%20Doe&state=CA&limit=10" \
-H "Authorization: Bearer $COVERFI_API_KEY"
Identity check (authenticated)
POST /orgs/{org_id}/records/check Cost: 1 credit
Body
{
"full_name": "Jane Doe",
"date_of_birth": "1984-03-15",
"email": "jane@example.com",
"phone": null,
"city": "Los Angeles",
"state": "CA",
"zip": "90001"
}
date_of_birth: YYYY-MM-DD or YYYYMMDD. Provide email and/or phone.
Response 200
{
"outcome": "match",
"match_confidence": 92,
"score": 92,
"signals": ["name", "dob", "email"],
"record_id": "GD-… or null",
"teaser": { "...": "RecordSummaryOut or null" }
}
outcome is one of: match | no_match | needs_review.
signals lists which submitted attributes agreed with the record; possible values are name, name_partial, dob, dob_year, email, email_similar, phone, phone_similar, location, location_partial, and ambiguous.
match_confidence (0–100) reflects how consistent the submitted details are with the matched record. score is a deprecated alias of match_confidence and will be removed in a future version.
curl -sS -X POST "https://coverfi.com/api/orgs/$ORG_ID/records/check" \
-H "Authorization: Bearer $COVERFI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"full_name":"Jane Doe","date_of_birth":"1984-03-15","email":"jane@example.com"}'
Record profile (detail)
GET /orgs/{org_id}/records/{record_id} Cost: 1 credit (auto-charged when the org has credits; degrades to a free masked profile when it does not)
record_id is the GD- public ID from search/check responses (legacy IDs still resolve). Returns full name parts, DOB, alternate names, addresses, phones, emails, IP/device ids.
- With credits: full unmasked profile,
masked: false. Re-fetching the
same record within 10 minutes does not charge again.
- `credits_charged`: what this specific request spent — mirrors the
X-Credits-Charged header in the response body, so clients that cannot read headers still know. 0 on a masked degrade, a re-fetch inside the 10-minute window, and /preview.
- Without credits:
200with the masked profile (preview: true,
masked: true, unmask hint) — see Masking semantics. A masked response is never charged and does not start the 10-minute free re-fetch window. The masked (degraded) response accepts ?city=1 to reveal city when the caller had searched by city, same as /preview. Funded (unmasked) responses ignore the param.
- First paid view of a record: if *your org* has never been charged for
this record, the endpoint returns 428 with {detail, credit_cost} until you resend with the X-Confirm-Shared-Access: true header. The check is org-scoped and says nothing about other orgs — despite the header name, it does not detect that a record was shared with you. The dashboard confirms on every view and surfaces a dismissible notice instead. A zero-credit org that confirms then receives the masked profile at no charge.
Full (unmasked) responses may include _meta (watermark) and header X-GD-Trace for leak attribution — do not strip these in production integrations.
curl -sS "https://coverfi.com/api/orgs/$ORG_ID/records/$RECORD_ID" \
-H "Authorization: Bearer $COVERFI_API_KEY"
People at address (household)
GET /orgs/{org_id}/records/at-address Cost: 2 credits
Query parameters
| Param | Required | Notes |
|---|---|---|
house_number | with street | |
street_name | with house | |
street_suffix | no | e.g. St, Ave |
unit_number | no | |
city / state | alt to zip | both if no zip |
zip | alt to city+state | |
current_only | no | default false |
limit / offset | no | max 25 |
Response includes items (masked contact fields), residency (current | historical), current_count, historical_count, address_key, has_more.
curl -sS "https://coverfi.com/api/orgs/$ORG_ID/records/at-address?house_number=123&street_name=Main&zip=90001" \
-H "Authorization: Bearer $COVERFI_API_KEY"
Recent queries
GET /orgs/{org_id}/records/recent?limit=20 Cost: free Returns the org’s recent search / check / view / address-lookup audit rows (no PII payloads beyond stored query params).
Public free identity check (no API key)
POST /records/check Rate limit: 1/hour per IP. No credits. Coarse outcome only — no record_id, no teaser.
curl -sS -X POST "https://coverfi.com/api/records/check" \
-H "Content-Type: application/json" \
-d '{"full_name":"Jane Doe","date_of_birth":"1984-03-15","email":"jane@example.com"}'
Response:
{
"outcome": "match",
"match_confidence": 90,
"score": 90,
"signals": ["identity"],
"signup_required": true
}
Permits & local activity
Preview. These endpoints are not yet generally available. While the preview gate is on, every call in this section and in Watches below requires a signed-in superuser session; API keys and other users receive 403 (ERR_LACT_003). The section opens to all keys when the gate is lifted, so the sk_live_… examples below apply from then on.
Building permits, code cases, certificates of use, contractor activity, and operating signals for places in Florida. All paths are under /api/orgs/{org_id}/… and accept an API key (Authorization: Bearer sk_live_…) or a session JWT.
Billing. The first request for a parcel or a license costs 2 credits (the property view / contractor view actions). Repeat requests for the same parcel or license within 24 hours are free, and sub-resources (/activity, /permits, /signals) are included in that window. A request that finds nothing (404) never charges. The neighborhoods activity layer and watches are free.
This data describes properties and licensed contractors. It is not a consumer report and must not be used to determine eligibility for credit, insurance, employment, or housing.
Get a property
GET /orgs/{org_id}/properties/{parcel_id}
parcel_id is the county parcel identifier (it contains a :; URL-encode it). Charges 2 credits on first view.
curl -sS "https://coverfi.com/api/orgs/$COVERFI_ORG_ID/properties/12086%3A0141230012340" \
-H "Authorization: Bearer sk_live_…"
{
"parcel_id": "12086:0141230012340",
"county_fips": "12086",
"county_name": "Example County",
"situs_address": "100 Sample St",
"situs_city": "Exampleville",
"situs_state": "FL",
"situs_zip": "33100",
"land_use": "0100",
"land_use_label": "Single family",
"assessed_value": 412000,
"acreage": 0.18,
"last_sale_price": 365000,
"last_sale_year": 2019,
"owner_name": "SAMPLE HOLDINGS LLC",
"owner_type": "company",
"counts": { "permits": 4, "code_cases": 1, "certificates_of_use": 0 },
"coverage": { "permits": true, "code_cases": true, "certificates_of_use": false }
}
coverage.<kind> is true when the county has at least one feed of that kind. When it is false, an empty list means "no feed yet", not "nothing happened".
Property activity
GET /orgs/{org_id}/properties/{parcel_id}/activity
Permits, code cases, and certificates of use for the parcel, newest first, up to 200 of each. Free if the property was viewed in the last 24 hours; otherwise it charges the property view.
{
"summary": {
"permit_count": 4,
"first_permit_year": 2017,
"total_job_value": 58200,
"last_permit": { "issued_date": "2025-08-14", "work_class": "roofing" },
"open_permits": 1,
"next_open_expiry": "2026-02-14"
},
"permits": [
{
"permit_key": "ex_permits:BP-2025-0042",
"permit_number": "BP-2025-0042",
"issued_date": "2025-08-14",
"work_class": "roofing",
"work_description": "Re-roof, shingle",
"job_value": 18500,
"status": "open",
"final_date": null,
"expires_date": "2026-02-14",
"contractor": { "license_number": "CCC1234567", "name": "SAMPLE ROOFING INC", "owner_builder": false }
}
],
"code_cases": [
{
"case_key": "ex_cases:CE-1001",
"case_number": "CE-1001",
"opened_date": "2024-03-02",
"case_type": "Overgrown vegetation",
"status": "closed",
"closed_date": "2024-04-10"
}
],
"certificates_of_use": []
}
work_class is one of roofing, remodel, hvac_mechanical, electrical, plumbing, pool, new_construction, demolition, solar, windows_doors, fence_site, sign, other. Permit status is open, final, expired, void, or unknown. Job value is what the applicant declared, not an appraisal. summary.total_job_value counts a value repeated across one project's sub-permits (same property, same value, same year) once. A permit filed by the owner rather than a licensed contractor shows "name": "Owner-builder", "license_number": null, "owner_builder": true.
Get a contractor
GET /orgs/{org_id}/contractors/{license_number}
Profile for a licensed contractor with the last 36 months of permit activity. Charges 2 credits on first view.
curl -sS "https://coverfi.com/api/orgs/$COVERFI_ORG_ID/contractors/CCC1234567" \
-H "Authorization: Bearer sk_live_…"
{
"license_number": "CCC1234567",
"class_label": "Certified Roofing Contractor",
"name": "SAMPLE ROOFING INC",
"dba_name": null,
"status": "current_active",
"original_date": "2009-05-12",
"expires_date": "2026-08-31",
"city": "Exampleville",
"zip5": "33100",
"activity": {
"permits_36m": 212,
"job_value_36m": 3150000,
"median_job_value_ytd": 14200,
"last_permit_date": "2026-09-30",
"permits_90d": 31,
"busiest_county": { "county_fips": "12086", "county_name": "Example", "abbrev": "EXA" },
"counties": [{ "county_fips": "12086", "county_name": "Example", "abbrev": "EXA" }],
"working": true,
"monthly": [{ "month": "2026-09", "permits": 11 }]
},
"work_mix": [{ "work_class": "roofing", "label": "Roofing", "permits": 180, "share": 0.85 }],
"by_county": [
{
"county_fips": "12086",
"county_name": "Example",
"permits": 212,
"job_value": 3150000,
"median_job": 14200,
"last_permit_date": "2026-09-30"
}
],
"recent_permits": { "items": [], "next_cursor": null, "total": 212 },
"business": { "business_key": "fl_sos:P09000012345", "name": "SAMPLE ROOFING INC", "status": "active" },
"tax_receipt": { "renewed_date": "2025-09-20", "jurisdiction_label": "Example County" },
"discipline": null
}
status is current_active, current_inactive, or other. County names are bare (no " County" suffix) and abbrev is a three-letter tag; busiest_county is the county with the most permits in 36 months, or null. monthly covers the 24 complete months before the dataset's snapshot month, zero-filled. job_value_36m and per-county job_value count a value repeated across one project's sub-permits once. working is true when the contractor pulled a permit in the last 30 days. business and tax_receipt are null when no match is found. discipline is reserved and always null.
Contractor permits
GET /orgs/{org_id}/contractors/{license_number}/permits?cursor=&limit=25
Pages through the contractor's permits, newest first. Pass the returned next_cursor as cursor for the next page; null means the last page. Same billing window as the profile.
{
"items": [
{
"permit_key": "ex_permits:BP-2026-0310",
"permit_number": "BP-2026-0310",
"issued_date": "2026-09-30",
"work_class": "roofing",
"work_description": "Re-roof, tile",
"job_value": 22000,
"status": "open",
"final_date": null,
"expires_date": null,
"parcel_id": "12086:0141230099990",
"address": "200 Sample Ave",
"city": "Exampleville",
"zip5": "33100"
}
],
"next_cursor": "MjAyNi0wOS0zMHxleF9wZXJtaXRzOkJQLTIwMjYtMDMxMA==",
"total": 212
}
Business operating signals
GET /orgs/{org_id}/businesses/{business_key}/signals
Evidence that a business is still operating: recent local tax receipts, permits, certificates of use, inspections, and contractor-license activity. Newest first, at most 25 signals from the last 36 months. Charged as a business view unless one was paid in the last 24 hours.
{
"operating": true,
"last_signal_date": "2026-09-30",
"working_address": "100 Sample St, Exampleville, FL 33100",
"signals": [
{
"date": "2026-09-30",
"kind": "permit_pulled",
"label": "Permit pulled",
"detail": "Re-roof, tile · $22,000",
"where": "200 Sample Ave, Exampleville",
"link": { "type": "property", "ref": "12086:0141230099990" }
}
]
}
kind is one of tax_receipt_renewed, tax_receipt_opened, permit_pulled, certificate_of_use, inspection, contractor_license. link.type is property or contractor; link itself is null when there is nothing to open. operating is true when any signal falls in the last 12 months. At most the three most recent permit_pulled signals are listed. working_address comes only from the business's own tax receipt, certificate of use or inspection (never a permit job site) and is null when there is none.
Neighborhood activity layer
Free area data; no credits.
GET /orgs/{org_id}/local-activity/zips?state=FL returns permit intensity per ZIP (join it onto your own ZIP geometry):
{
"breaks": [4.1, 7.8, 12.5, 19.0],
"items": [{ "zip5": "33100", "permits_12m": 812, "permits_per_1k": 14.2, "quintile": 4, "has_feed": true }]
}
GET /orgs/{org_id}/local-activity/zips/{zip5} returns the profile for one ZIP:
{
"zip5": "33100",
"quintile": 4,
"has_feed": true,
"permits_12m": 812,
"permits_yoy": 0.06,
"job_value_12m": 14600000,
"new_tax_receipts_12m": 93,
"open_code_cases": 41,
"monthly": [{ "month": "2026-09", "permits": 70 }],
"work_mix": [{ "work_class": "roofing", "label": "Roofing", "share": 0.31 }],
"top_contractors": [
{ "license_number": "CCC1234567", "name": "SAMPLE ROOFING INC", "permits": 38, "main_work": "Roofing" }
]
}
main_work is the display label of the contractor's most common work class. monthly covers the 24 complete months before the dataset's snapshot month, zero-filled; job_value_12m counts a value repeated across one project's sub-permits once. Owner-builders never appear in top_contractors. ZIPs in counties without a feed return has_feed: false and quintile: null.
Errors for these endpoints
| Code | Status | Meaning |
|---|---|---|
ERR_PROP_004 | 404 | Property not found — the parcel_id does not exist. Nothing is charged. |
ERR_LACT_001 | 404 | Contractor not found — the license number is not on file. Nothing is charged. |
ERR_LACT_002 | 503 | Local activity data is temporarily unavailable. Retry shortly; nothing is charged. |
ERR_LACT_003 | 403 | Permits and local activity are in internal preview. Returned to API keys and non-superuser sessions while the preview gate is on. Not retryable. |
Credit and authentication errors are the same as for every other endpoint (see Errors below).
Watches
Watches monitor places and licenses for new activity. They are free and are managed by org members. They share the preview gate described above. Matching runs once a day; hits are de-duplicated per watch.
A watch has a name (up to 80 characters), a where_kind, up to 20 where_values, and triggers:
where_kind | where_values are | ||
|---|---|---|---|
zip | 5-digit ZIP codes | ||
county | county FIPS codes | ||
city | `"CITY\ | FL" strings, e.g. "EXAMPLEVILLE\ | FL"` |
license | contractor license numbers | ||
business | business keys | ||
parcel | parcel IDs |
{
"name": "New roofing permits, 33100",
"where_kind": "zip",
"where_values": ["33100"],
"triggers": {
"new_businesses": false,
"new_permits": { "enabled": true, "min_job_value": 10000 },
"code_cases": false,
"str_registrations": false,
"license_changes": false
}
}
| Endpoint | Description |
|---|---|
GET /orgs/{org_id}/watches | { "items": [Watch], "limit": 50 }. Each Watch is the body above plus id, status (active or paused), matched_through, created_by, created_at, updated_at, and hits_7d. |
POST /orgs/{org_id}/watches | Create a watch (body above). Returns the Watch with 201. At most 50 per org. |
PATCH /orgs/{org_id}/watches/{id} | Partial update, including status. |
DELETE /orgs/{org_id}/watches/{id} | Delete a watch and its hits. 204. |
POST /orgs/{org_id}/watches/preview | Body as for create. Returns { "matches_7d": 12 } without saving. |
GET /orgs/{org_id}/watches/hits?cursor=&limit=25 | Newest hits across all watches. |
curl -sS -X POST "https://coverfi.com/api/orgs/$COVERFI_ORG_ID/watches/preview" \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{"name":"New roofing permits, 33100","where_kind":"zip","where_values":["33100"],"triggers":{"new_businesses":false,"new_permits":{"enabled":true,"min_job_value":10000},"code_cases":false,"str_registrations":false,"license_changes":false}}'
{
"items": [
{
"id": "7b0f6a52-0000-4000-8000-000000000001",
"watch_id": "7b0f6a52-0000-4000-8000-000000000002",
"watch_name": "New roofing permits, 33100",
"occurred_on": "2026-10-06",
"event_kind": "permit",
"label": "Permit pulled",
"detail": "Re-roof, tile · $22,000",
"where_text": "200 Sample Ave, Exampleville",
"link": { "type": "property", "ref": "12086:0141230099990" }
}
],
"next_cursor": null,
"total": 1
}
event_kind is one of new_business, permit, code_case, str_registration, license_change.
| Code | Status | Meaning |
|---|---|---|
ERR_WATCH_001 | 404 | Watch not found. |
ERR_WATCH_002 | 400 | A watch needs a place and something to watch (field errors on where / triggers). |
ERR_WATCH_003 | 409 | Watch limit reached (50 per org). |
Errors
Failures return RFC 9457 problem+json, e.g.:
{
"type": "https://coverfi.com/errors/ERR_AUTH_003",
"title": "…",
"status": 401,
"detail": "…",
"instance": "/api/…",
"code": "ERR_AUTH_003"
}
Correlation id is also in X-Request-ID. Common cases:
| Situation | Typical status |
|---|---|
| Missing/invalid/revoked API key | 401 |
| Wrong org for key / no access | 404 (org not found) |
| Insufficient credits | 4xx credit error |
| Insufficient search/check/address criteria | 4xx |
| Rate limited | 429 |
| Preview-gated endpoint (see Permits & local activity) | 403 |
| Org suspended | 403 |
Mutating requests from the web app send an Idempotency-Key; API clients may do the same for safe retries.
Compliance (required)
CoverFi records data is not a consumer report under the FCRA. You must not use API data to make decisions about consumer eligibility for credit, employment, insurance, housing, tenant screening, background checks, or any other purpose covered by the FCRA or similar laws. Creating an API key requires attesting to this. See Terms: https://coverfi.com/terms
Integration checklist for coding agents
- Store
COVERFI_API_KEYandCOVERFI_ORG_IDas secrets — never commit keys. - Always send
Authorization: Bearer $COVERFI_API_KEY. - Enforce match-in rules client-side before calling (saves credits on 4xx).
- Budget credits: search = 1, profile = 1, address = 2; identity check is free.
- Check `masked` on every search/profile response before treating it as
complete — a zero-credit org gets 200 with masked data, not an error.
- Store the
GD-record ID from step 1 and pass it to step 2 — it is
CoverFi's permanent identifier for that person.
- Paginate with
offset+has_more(no total count on search). - Treat search
email_masked/phone_maskedas incomplete; fetch the
profile when you need full contact fields.
- Handle
401/429/428(first-paid-view confirm) with clear user messaging. - Prefer this markdown file (
/docs/api.md) as the source of truth over scraping HTML.
Links
| Resource | URL |
|---|---|
| Human docs | https://coverfi.com/docs |
| This file | https://coverfi.com/docs/api.md |
| LLM index | https://coverfi.com/llms.txt |
| OpenAPI spec (best-effort shapes; this file is normative) | https://coverfi.com/docs/openapi.json |
| API keys UI | https://coverfi.com/api-keys |
| Pricing | https://coverfi.com/pricing |