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

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:

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

(contact fields stay teaser-masked at the list level — their values come from the profile).

but echo-only masked — see Masking semantics.

person by their GD- ID (or via the row's detail_url).

is returned. Re-fetching the same record within 10 minutes is free.

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)

HeaderValue
AuthorizationBearer sk_live_…

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)

MethodPathRole
POST/orgs/{org_id}/api-keysAdmin
GET/orgs/{org_id}/api-keysMember
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.

ActionCreditsEndpoints
Record search1GET …/records
Record profile (detail)1GET …/records/{record_id}
People at address2GET …/records/at-address
Identity check0 (free)POST …/records/check
Record preview0 (free)GET …/records/{record_id}/preview
Recent queries0 (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:

HeaderMeaning
X-Credits-ChargedCredits captured by this request (0 on masked, deduped, refunded, preview, and 428 responses)
X-Credits-BalanceOrg 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

MethodPathAuthPurpose
GET/credits/actionsnonePublic catalog of action costs
GET/orgs/{org_id}/creditsAPI key or JWTWallet: {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.

EndpointLimit
Search30/minute
Identity check (auth)20/minute
People at address30/minute
Record detail60/minute
Recent queries60/minute
Public free check1/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:

Identity check (POST …/records/check)

People at address (GET …/records/at-address)


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:

object: { "credit_cost": 1, "purchase_url": "https://coverfi.com/billing", "message": "…" }.

`"Buy credits to unmask"`; fields with no value stay null.

to null. List fields keep their counts, with each entry's values replaced by the phrase.

(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:

complete; email_masked / phone_masked always stay at teaser fidelity on lists. Remedy: fetch_record (use the row's detail_url / next_step).

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

ParamTypeNotes
qstringName free text — "First Last" or "Last, First". Two-token queries without a comma try both orderings automatically.
citystring
statestring2-letter US state
zipstring
emailstring
phonestring
dobstringYYYYMM / YYYYMMDD prefix match
age_min / age_maxint0–130
deceasedstringtrue / false / 1 / 0
limitint1–25 (free plan capped at 5)
offsetintPagination

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.

same record within 10 minutes does not charge again.

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.

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.

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

ParamRequiredNotes
house_numberwith street
street_namewith house
street_suffixnoe.g. St, Ave
unit_numberno
city / statealt to zipboth if no zip
zipalt to city+state
current_onlynodefault false
limit / offsetnomax 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

CodeStatusMeaning
ERR_PROP_004404Property not found — the parcel_id does not exist. Nothing is charged.
ERR_LACT_001404Contractor not found — the license number is not on file. Nothing is charged.
ERR_LACT_002503Local activity data is temporarily unavailable. Retry shortly; nothing is charged.
ERR_LACT_003403Permits 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_kindwhere_values are
zip5-digit ZIP codes
countycounty FIPS codes
city`"CITY\FL" strings, e.g. "EXAMPLEVILLE\FL"`
licensecontractor license numbers
businessbusiness keys
parcelparcel 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
  }
}
EndpointDescription
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}/watchesCreate 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/previewBody as for create. Returns { "matches_7d": 12 } without saving.
GET /orgs/{org_id}/watches/hits?cursor=&limit=25Newest 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.

CodeStatusMeaning
ERR_WATCH_001404Watch not found.
ERR_WATCH_002400A watch needs a place and something to watch (field errors on where / triggers).
ERR_WATCH_003409Watch 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:

SituationTypical status
Missing/invalid/revoked API key401
Wrong org for key / no access404 (org not found)
Insufficient credits4xx credit error
Insufficient search/check/address criteria4xx
Rate limited429
Preview-gated endpoint (see Permits & local activity)403
Org suspended403

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

complete — a zero-credit org gets 200 with masked data, not an error.

CoverFi's permanent identifier for that person.

profile when you need full contact fields.


ResourceURL
Human docshttps://coverfi.com/docs
This filehttps://coverfi.com/docs/api.md
LLM indexhttps://coverfi.com/llms.txt
OpenAPI spec (best-effort shapes; this file is normative)https://coverfi.com/docs/openapi.json
API keys UIhttps://coverfi.com/api-keys
Pricinghttps://coverfi.com/pricing