Skip to main content

API and webhooks

Everything you get in your weekly file, delivered to your CRM. The API returns the same records and the same columns as your download, for the counties on your subscription, with the same suppressions applied. Manage keys and webhooks in the Integrations section of your dashboard.

Authentication

Create a key in your dashboard. It looks like cl_live_ followed by 32 characters, and it is shown once: store it somewhere safe. Send it on every request:

Authorization: Bearer cl_live_YOUR_KEY

Errors

Every error has the same shape. 401 means the key is missing, wrong or revoked (we do not say which). 403 means your subscription is not active, or the county is not one you hold. 400 is a bad parameter and 429 is the rate limit.

{
  "error": {
    "code": "unauthorized",
    "message": "Missing or invalid API key. Send it as \"Authorization: Bearer cl_live_...\"."
  }
}

GET /api/v1/counties

The counties your key may read, with their id, record type and last delivery.

curl https://countyleads.ai/api/v1/counties \
  -H "Authorization: Bearer cl_live_YOUR_KEY"
{
  "data": [
    {
      "id": "harris-tx-probate",
      "name": "Harris",
      "state": "TX",
      "record_type": "probate",
      "delivery_frequency": "weekly",
      "last_delivered_at": "2026-11-02T12:00:00.000Z"
    }
  ]
}

GET /api/v1/records

Records for one county you hold, newest first.

ParameterMeaning
countyRequired. A county id from the counties endpoint.
sinceOptional ISO 8601 date. Only records retrieved after it. Defaults to the last 7 days; you cannot read further back than 90 days, or from before the first week of data your county subscription covers.
limitOptional. 1 to 500, default 100.
cursorOptional. The next_cursor from the previous page.
curl "https://countyleads.ai/api/v1/records?county=harris-tx-probate&since=2026-10-26T00:00:00Z&limit=100" \
  -H "Authorization: Bearer cl_live_YOUR_KEY"
{
  "data": [
    {
      "id": "0b6f3c1e-8f0e-4d0a-9a53-2f6a1d7c9b11",
      "retrieved_at": "2026-11-02T06:14:09.000Z",
      "record_type": "probate",
      "county": "Harris",
      "date_filed": "2026-10-30",
      "case_number": "123456",
      "party_name": "...",
      "address": "...",
      "street_address": "...",
      "city": "Houston",
      "state": "TX",
      "zip": "77002",
      "case_type": "Probate of Will",
      "decedent": "...",
      "date_of_death": "2026-09-18",
      "petitioner": "...",
      "attorney": null,
      "document_link": null
    }
  ],
  "next_cursor": "eyJhdCI6...",
  "disclaimer": "..."
}

When next_cursor is not null there is another page. Pass it back unchanged. Treat it as opaque.

curl "https://countyleads.ai/api/v1/records?county=harris-tx-probate&limit=100&cursor=eyJhdCI6..." \
  -H "Authorization: Bearer cl_live_YOUR_KEY"

Columns match your Excel file for that record type, lower-cased with underscores (for example case_number, party_name, date_of_death). Empty cells are null. Every record also has an id and retrieved_at. Every response includes a disclaimer: keep it with the data. The records are public-record information and are not a consumer report; see the FCRA notice before you use them.

Webhooks

Coming soon. Webhooks are not switched on yet. Until they are, pull new records with GET /api/v1/records, or use a Zapier or Make schedule that calls it.

Add an https URL in your dashboard. After each delivery to you we POST a records.delivered event for each county, with at most 500 records per event (a larger delivery is split into several events; chunk tells you which). The records are the same ones in your email, with the same suppressions applied.

{
  "event_id": "evt_3f9a1c0b7d2e4a6b8c1d0e2f",
  "event": "records.delivered",
  "county": "harris-tx-probate",
  "county_name": "Harris",
  "run_id": "run_abc123",
  "delivered_at": "2026-11-02T12:00:00.000Z",
  "chunk": { "index": 0, "count": 1 },
  "records": [ { "id": "...", "case_number": "...", "party_name": "..." } ],
  "disclaimer": "..."
}

Verify the signature

We follow the open Standard Webhooks scheme. Each request has three headers, and the signing secret (whsec_...) is in your dashboard under the webhook.

webhook-id: evt_3f9a1c0b7d2e4a6b8c1d0e2f
webhook-timestamp: 1793620800
webhook-signature: v1,K5oZfzN0m3...base64...

The signature is v1, plus the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64 part of your secret. Check it on the raw body before you parse it.

import { createHmac, timingSafeEqual } from 'node:crypto'

// secret is the "whsec_..." value from your dashboard. rawBody is the exact
// request body text: verify BEFORE you parse the JSON.
export function verifyWebhook(secret, headers, rawBody) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatures = String(headers['webhook-signature'] ?? '').split(' ')

  // Reject old or far-future messages (replay protection): 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`, 'utf8')
    .digest()

  return signatures.some((entry) => {
    const [version, value] = entry.split(',')
    if (version !== 'v1' || !value) return false
    const given = Buffer.from(value, 'base64')
    return given.length === expected.length && timingSafeEqual(given, expected)
  })
}

Or use the standardwebhooks library:

import { Webhook } from 'standardwebhooks'

const wh = new Webhook(process.env.COUNTYLEADS_WEBHOOK_SECRET) // "whsec_..."
const event = wh.verify(rawBody, {
  'webhook-id': headers['webhook-id'],
  'webhook-timestamp': headers['webhook-timestamp'],
  'webhook-signature': headers['webhook-signature'],
})

Zapier

  1. In Zapier, make a Zap whose trigger is Webhooks by Zapier, event Catch Hook. Copy the custom webhook URL.
  2. In your CountyLeads dashboard, add that URL under Webhooks and press Send test event. Zapier will show the test request.
  3. Add a Looping by Zapier step over records so each record becomes one run, then map the fields (party_name, address, case_number) into your CRM action.
  4. Zapier cannot check the signature. The URL is secret and long: treat it like a password and replace it if it leaks.

Make

  1. Add a Webhooks > Custom webhook module and copy its address.
  2. Add that address in your dashboard and press Send test event so Make learns the shape.
  3. Add an Iterator on records, then your CRM module. Map each field from the iterator output.

GoHighLevel

Use a GoHighLevel workflow with the Inbound Webhook trigger (or a Zapier or Make scenario that calls Create Contact). Suggested mapping from a probate record:

CountyLeads fieldGoHighLevel field
party_nameFull name (split into first and last name)
addressAddress, city, state and postal code
case_numberCustom field: Case number
decedent, date_of_deathCustom fields: Decedent, Date of death
county, date_filedCustom fields: County, Date filed (or a tag such as the county id)
idCustom field: CountyLeads record id (use it to avoid duplicates)

Create the custom fields in GoHighLevel first. To fill in missing contact details, skip trace the records with your provider of choice; we do not supply phone numbers or emails through the API.

Help

Questions or a field you need? Email support@countyleads.ai.