CompCurve License API docs · v1

Your key:

# URLs

API basehttps://compcurve-license-api.eli-ded.workers.dev Dashboardhttps://compcurve-license-dashboard.pages.dev Healthhttps://compcurve-license-api.eli-ded.workers.dev/v1/health no auth

Versioned under /v1. Backwards-incompatible changes will get a /v2.

# Quickstart

Paste your key in the box at the top right — all curl samples on this page auto-fill with it. The simplest authenticated call:

curl
curl -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/me

Successful response (HTTP 200):

"data": {
  "key_id": 1,
  "key_prefix": "cck_NFcP9qQ2",
  "label": "Smoke test (Eli)",
  "tier": "pro",
  "rate_per_min": 300,
  "rate_per_day": 50000
}

# Authentication

Every /v1/* endpoint except /v1/health requires a key. Three ways to send it, in priority order:

  1. Authorization: Bearer cck_... preferred
  2. X-API-Key: cck_...
  3. ?api_key=cck_... discouraged — leaks into logs/Referer

Three identical calls

Bearer header (preferred)
curl -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/me
X-API-Key
curl -H "X-API-Key: YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/me
Query parameter (last resort)
curl "https://compcurve-license-api.eli-ded.workers.dev/v1/me?api_key=YOUR_KEY"

Keys are stored server-side as SHA-256 hashes. If you lose one, request a new one — the original cannot be re-displayed. Revoke unused keys promptly.

# Response envelope

Every successful response has the same top-level shape:

{
  "data": ,           // the payload (array, object, or single record)
  "pagination": {  },   // only on list endpoints
  "meta": {  }          // what the server actually applied (snapshot, sort, filters)
}

meta.filters_applied is especially useful when debugging — it shows which filter params the server recognized.

# Errors

Errors return non-2xx HTTP status and this shape:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded (60 per minute). Retry at 2026-05-10T17:45:00.000Z."
  }
}

Common codes

HTTPerror.codeWhen
400invalid_sortTried to sort on a column not in the whitelist.
400invalid_fieldAsked for a field in ?fields= that doesn't exist.
400invalid_dateDate param isn't YYYY-MM-DD.
400invalid_orderorder wasn't asc or desc.
400offset_too_deeppage × limit exceeded MAX_OFFSET (50,000). Filter harder.
400no_snapshotAsked for a snapshot that hasn't been loaded.
401unauthorizedMissing, malformed, invalid, or revoked key.
404not_foundLicense ID doesn't exist, or unknown route.
405method_not_allowedOnly GET is supported.
429rate_limitedYou've exceeded your per-minute or per-day cap.
503service_unavailableBackend store is temporarily down (e.g. mid-import).

# Rate limits

Default tier: 60/min, 1,000/day. Pro tier: 300/min, 50,000/day. Every authenticated response includes:

HeaderMeaning
RateLimit-LimitCurrent window's cap.
RateLimit-RemainingRequests left in this window.
RateLimit-ResetSeconds until the window rolls over.
RateLimit-PolicyE.g. 300;w=60 — 300 requests per 60s window.
X-RateLimit-Windowminute or day — which limit is currently tightest.
Retry-AfterOnly on 429 responses. Seconds to wait.
See your rate-limit state with -i
curl -i -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/me

# Pagination

Offset-based via page + limit. Defaults: page=1, limit=100. Max limit=1000. page × limit is capped at 50,000 — past that, filter harder.

Response includes:

"pagination": {
  "page": 3,
  "limit": 100,
  "total": 1703,        // matches across the entire result set
  "total_pages": 18,
  "has_next": true,
  "has_prev": true
}
Fetch page 3, 100 rows per page
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA&last_name=smith&page=3&limit=100"

# Sort

?sort=col1,col2&order=asc|desc. Single direction applies to all columns. id is always appended as a tie-breaker for stable pagination.

Whitelisted sort columns:

id · state · member_last_name · member_first_name · member_full_name · member_state_license_number · member_license_status · member_license_issued · member_license_expires · member_license_first_issued · office_name · last_refreshed

Active CA licensees sorted by soonest expiration
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA&license_status=Licensed&sort=member_license_expires&order=asc&limit=5"

# Sparse fieldsets

By default /v1/licenses returns all 30 columns per row. Pass ?fields=col1,col2,... to slim the payload — ideal for dashboards and large pages.

Just name and license #
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA&last_name=smith&fields=member_full_name,member_state_license_number&limit=5"
{
  "data": [
    { "id": 406322, "member_full_name": "Aaron Joseph Smith II", "member_state_license_number": "02145091" },
    { "id": 426253, "member_full_name": "Aaron Matthew Smith", "member_state_license_number": "02189531" }
  ],
  …
}

id is always included even if omitted from fields — you need it for follow-up /v1/licenses/:id calls.

# Filter reference

All optional, AND-combined. Empty / missing = no constraint on that field.

ParameterBehaviorExample
stateExact match; comma-separable.state=CA or state=CA,TX,FL
countryExact (US or CA).country=US
last_nameCase-insensitive prefix.last_name=smith
office_nameCase-insensitive substring.office_name=keller
license_numberExact.license_number=00007012
license_statusExact (taxonomies vary by state — see caveats).license_status=Active
member_typeExact.member_type=Broker
cityExact.city=Orinda
postal_codeExact (alias: zip).postal_code=94563
issued_after / issued_beforeISO date range (inclusive).issued_after=2024-01-01
expires_after / expires_beforeISO date range (inclusive).expires_before=2026-12-31
first_issued_after / first_issued_beforeISO date range.first_issued_after=2010-01-01
snapshotPick a specific month. Defaults to the latest loaded.snapshot=2026-05

# GET /v1/health no auth

Lightweight liveness check. Returns DB reachability and snapshot count. No rate limit.

curl https://compcurve-license-api.eli-ded.workers.dev/v1/health
{
  "status": "ok",
  "db": true,
  "snapshots_loaded": 1,
  "time": "2026-05-10T17:37:27.924Z",
  "version": "v1"
}

status is "degraded" if the DB query failed (rare; usually means a mid-import lock).

# GET /v1/me auth required

Echoes your key's metadata and configured rate limits. Useful in scripts to confirm the key is valid and to learn its quotas.

curl -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/me
{
  "data": {
    "key_id": 1,
    "key_prefix": "cck_NFcP9qQ2",
    "label": "Smoke test (Eli)",
    "tier": "pro",
    "rate_per_min": 300,
    "rate_per_day": 50000
  }
}

# GET /v1/snapshots auth required

List of loaded monthly snapshots with row counts. Sorted newest first.

curl -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/snapshots
{
  "data": [
    {
      "snapshot": "2026-05",
      "loaded_at": "2026-05-10 17:36:47",
      "us_row_count": 2339569,
      "ca_row_count": 177272,
      "total_row_count": 2516841,
      "notes": null
    }
  ]
}

# GET /v1/states auth required

Row counts per state / province for a snapshot. Useful for navigation UIs and sanity-checks. Filter by country if you only need one side.

Query params

snapshotDefaults to latest loaded.
countryUS or CA. Omit for both.
All Canadian provinces, current snapshot
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/states?country=CA"
{
  "data": [
    { "country": "CA", "state": "AB",  "row_count": 18072 },
    { "country": "CA", "state": "BC",  "row_count": 29003 },
    { "country": "CA", "state": "ON",  "row_count": 103199 },
    { "country": "CA", "state": "QC",  "row_count": 16196 }
    // + 7 more provinces/territories
  ],
  "meta": { "snapshot": "2026-05", "country": "CA" }
}

# GET /v1/licenses auth required

The main query endpoint. Combines filters + sort + pagination + sparse fieldsets. See the filter reference for all parameters.

Example 1 — all Brokers in Florida with a Smith last name

curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=FL&last_name=smith&member_type=Broker&limit=3"

Example 2 — active CA licensees expiring in 2026, sorted by date

curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA&license_status=Licensed&expires_after=2026-05-10&expires_before=2026-12-31&sort=member_license_expires&order=asc&limit=10"

Example 3 — multi-state, sparse fieldsets, page 2

curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA,TX,FL&license_status=Licensed&fields=member_full_name,state,member_state_license_number,member_license_expires&page=2&limit=100"

Sample response (truncated)

{
  "data": [
    {
      "id": 406322,
      "snapshot": "2026-05",
      "country": "US",
      "state": "CA",
      "member_full_name": "Aaron Joseph Smith II",
      "member_first_name": "Aaron Joseph",
      "member_last_name": "Smith II",
      "member_type": "Salesperson",
      "member_city": "NEWPORT BEACH",
      "member_state_or_province": "CA",
      "member_postal_code": "92660",
      "office_name": "COLDWELL BANKER REALTY",
      "member_state_license_number": "02145091",
      "member_license_status": "Licensed",
      "member_license_issued": "2023-10-23",
      "member_license_expires": "2029-10-22",
      "member_license_first_issued": "2023-10-23",
      "last_refreshed": "2026-05-01"
    }
  ],
  "pagination": { "page": 1, "limit": 3, "total": 1703, "total_pages": 568, "has_next": true, "has_prev": false },
  "meta": { "snapshot": "2026-05", "sort": ["id"], "order": "asc", "fields": null, "filters_applied": ["state", "last_name"] }
}

# GET /v1/licenses/:id auth required

Fetch one record by its internal numeric id. The id comes from any /v1/licenses response. Returns 404 if no row matches.

curl -H "Authorization: Bearer YOUR_KEY" \
  https://compcurve-license-api.eli-ded.workers.dev/v1/licenses/406322

Returns the full record (all 30 columns), unwrapped from any list/pagination metadata: { "data": { ...record... } }.

# Recipe — page through all results

Walk the entire match set with a bash loop. Note the has_next check.

KEY=YOUR_KEY
URL=https://compcurve-license-api.eli-ded.workers.dev
PAGE=1
while : ; do
  RES=$(curl -s -H "Authorization: Bearer $KEY" \
    "$URL/v1/licenses?state=CA&last_name=smith&fields=member_full_name,member_state_license_number&limit=500&page=$PAGE")
  echo "$RES" | jq -r '.data[] | [.member_state_license_number, .member_full_name] | @tsv'
  HAS_NEXT=$(echo "$RES" | jq -r '.pagination.has_next')
  [ "$HAS_NEXT" != "true" ] && break
  PAGE=$((PAGE + 1))
done

For result sets larger than 50,000 rows you'll hit offset_too_deep — split by state, by status, or by date range to stay under the cap.

# Recipe — convert response to CSV

curl -s -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=NY&license_status=Active&fields=member_full_name,member_state_license_number,member_license_expires&limit=1000" \
  | jq -r '
      .data[0] as $first | $first | keys_unsorted | @csv,
      (.. | objects | select(has(($first|keys_unsorted)[0])) | [.[($first|keys_unsorted)[]]] | @csv)
    ' 2>/dev/null \
  | head

For more than 1,000 rows, combine with the paging recipe above.

# Recipe — find licensees expiring in the next 60 days

TODAY=$(date -u +%Y-%m-%d)
SOON=$(date -u -v +60d +%Y-%m-%d 2>/dev/null || date -u -d "+60 days" +%Y-%m-%d)
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=CA&license_status=Licensed&expires_after=$TODAY&expires_before=$SOON&sort=member_license_expires&order=asc&fields=member_full_name,member_state_license_number,member_license_expires,office_name&limit=50"

Returns CA licensees whose status is Licensed and who expire within the next 60 days, sorted by soonest first.

# Recipe — bulk lookup by license #

v1 has no batch endpoint yet, so today you fire one request per license number. Up to 300/min on the pro tier:

KEY=YOUR_KEY
URL=https://compcurve-license-api.eli-ded.workers.dev
for L in 02145091 02189531 02194195 02197976; do
  curl -s -H "Authorization: Bearer $KEY" \
    "$URL/v1/licenses?state=CA&license_number=$L&fields=member_full_name,office_name,member_license_status,member_license_expires" \
  | jq '.data[0]'
done

A POST batch endpoint is on the roadmap — see the future enhancements in the README.

# Recipe — discover statuses for a state

Until /v1/license-statuses exists, you can probe a state by sampling rows. Most states cluster around a small number of distinct values.

curl -H "Authorization: Bearer YOUR_KEY" \
  "https://compcurve-license-api.eli-ded.workers.dev/v1/licenses?state=ON&fields=member_license_status&limit=200" \
  | jq '[.data[].member_license_status] | group_by(.) | map({status: .[0], n: length}) | sort_by(.n) | reverse'

For Ontario you'd see REGISTERED as the dominant value. See caveats for the cheat sheet of per-state statuses.

# Field reference

Every license record has these columns (also queryable via ?fields=):

FieldTypeDescription
idintegerInternal stable ID across a single snapshot. Use for follow-up /v1/licenses/:id.
snapshotstringYYYY-MM. The monthly snapshot this row belongs to.
countrystringUS or CA.
statestring2-letter state / province code (3-letter for PEI).
member_full_namestringFull name as supplied by the source jurisdiction.
member_first_name, _middle_name, _last_name, _name_prefix, _name_suffix, _nicknamestringParsed name components.
member_typestringE.g. Broker, Salesperson. Varies by jurisdiction.
member_address_1, _address_2, _city, _state_or_province, _postal_codestringLicensee's address of record.
office_broker_name, office_name, office_key_numericstringThe licensee's office of record.
member_state_license_numberstringThe official license number issued by the state. Format varies.
member_license_statusstringThe current status. Per-state taxonomy — see caveats.
member_secondary_license_statusstringOptional secondary status (e.g. Active).
member_license_issueddateISO YYYY-MM-DD. Current term's issue date.
member_license_expiresdateISO YYYY-MM-DD. Current term's expiration.
member_license_first_issueddateISO YYYY-MM-DD. The earliest known issue date for this licensee.
member_disciplinedstringDisciplinary indicator (when supplied by the source).
member_preferred_phone, member_emailstringContact info, when supplied.
last_refresheddateWhen this jurisdiction's data was last refreshed in the source pipeline.

# Caveats & gotchas

License-status taxonomies vary by jurisdiction

There is no universal "active" value. Examples:

State / ProvinceTypical "active" value(s)
California (CA)Licensed, Licensed NBA
Ontario (ON)REGISTERED
Alaska (AK)Active
Florida (FL)Active
New York (NY)Active

Use the status-discovery recipe to confirm before relying on a value.

Dates are normalized to ISO YYYY-MM-DD

The upstream raw CSVs use MM/DD/YYYY; the API converts everything to ISO so dates sort correctly and date-range filters work.

50,000-row deep-paging cap

Past page × limit = 50,000 the API returns 400 offset_too_deep. Narrow your filter or split the query.

Monthly snapshots are point-in-time

A row's contents reflect the source's data as of the snapshot. Status changes mid-month aren't visible until the next snapshot loads.