# CompCurve License API

> Read-only REST API over monthly US + Canada real-estate license data —
> ~2.5M licenses per monthly snapshot across 62 jurisdictions. US rows are
> enriched with matched email/phone (~76% email coverage) and office agent
> counts. Data refreshes monthly; prior snapshots stay queryable.

- Base URL: https://api.compcurve.com
- Human docs + playground: https://api.compcurve.com/docs
- This file: https://api.compcurve.com/docs.md (alias: /llms.txt)
- OpenAPI 3.1 spec: https://api.compcurve.com/openapi.json
- Free trial key (self-serve, masked contacts): https://api.compcurve.com/start
- Your key's dashboard (limits, quota, scope, usage): https://api.compcurve.com/dashboard
- Plans / self-serve upgrade: https://api.compcurve.com/pricing · enterprise: eli@compcurve.com
- Agents & MCP (remote MCP server at https://api.compcurve.com/mcp): https://api.compcurve.com/agents

## Authentication

Every endpoint except `/v1/health` requires an API key (format `cck_…`),
sent as `Authorization: Bearer <key>` (preferred) or `X-API-Key: <key>`.

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=CA&limit=5" \
  -H "Authorization: Bearer cck_YOUR_KEY"
```

Rate limits are per-key (per-minute + per-day). Every response includes
`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`; HTTP 429
includes `Retry-After`. Check consumption at `GET /v1/usage`.
Demo-tier keys receive masked email/phone and no street address.

Keys may also be **scoped** to specific countries/states and/or a licensed
set of columns. A scoped key's queries are automatically narrowed to its
grant; asking for out-of-scope states or fields returns HTTP 403 with code
`scope_denied` and a message listing the grant. `GET /v1/me` shows your
key's `scope` (null values = unrestricted).

### Historical snapshots (history add-on)

Every key reads the **latest** monthly snapshot. Access to prior months is a
separate entitlement, `scope.history` on `GET /v1/me`:

| mode | meaning |
| --- | --- |
| `latest` | current snapshot only (default) |
| `months` | latest + N prior months (`months: 3` with latest 2026-08 → 2026-05…2026-08) |
| `all` | every loaded snapshot |

With history access you can pass `?snapshot=YYYY-MM` to `/v1/licenses`,
`/v1/states`, `/v1/stats`, `/v1/suggest`, read prior-month rows by id, and
call `GET /v1/licenses/history` — one license across all your accessible
months with a change log. Outside your scope the API returns HTTP 403
`history_denied` with a message stating what your key can read.
`GET /v1/snapshots` lists every loaded month and flags each `accessible`
true/false for your key.

## Endpoints

| Method | Path | Purpose | Auth |
| --- | --- | --- | --- |
| GET | /v1/health | Liveness + DB reachability | none |
| GET | /v1/me | Your key's tier + configured rate limits | key |
| GET | /v1/usage?days=30 | Your request counts by day + by endpoint (max 90) | key |
| GET | /v1/snapshots | Loaded monthly snapshots + row counts, each flagged `accessible` for your key | key |
| GET | /v1/states | States/provinces + per-state row counts (`?country=US` or `CA`) | key |
| GET | /v1/stats | Precomputed aggregates: status/member-type vocab, contact coverage, expiration buckets | key |
| GET | /v1/suggest | Type-ahead: distinct prefix matches for a field (`?field=city|office|last_name&q=kel&state=CA`) — for autocomplete UIs | key |
| GET | /v1/licenses | Main query endpoint — filters, sort, pagination, sparse fields | key |
| GET | /v1/licenses/{id} | Single license by internal id (id is per-snapshot) | key |
| GET | /v1/licenses/history | One license across every accessible snapshot + field-level change log (`?state=TX&license_number=…` or `?id=…`) — **history add-on** | key |
| POST | /mcp | Remote MCP server (JSON-RPC 2.0 over HTTP, stateless): tools search_licenses, get_license, license_history, list_states, list_snapshots, get_stats, suggest, whoami. Setup: https://api.compcurve.com/agents | key |

## GET /v1/licenses — query parameters

All filters are optional and AND-combined.

| Param | Behavior | Example |
| --- | --- | --- |
| snapshot | Monthly snapshot, e.g. 2026-07. Defaults to latest. Prior months require the history add-on — otherwise 403 history_denied (see /v1/me → scope.history) | snapshot=2026-07 |
| state | Exact state/province code; comma-separable. e.g. CA,TX | state=CA,TX |
| country | US or CA | country=US |
| last_name | Case-insensitive prefix match | last_name=smith |
| office_name | Case-insensitive prefix match (use /v1/suggest for autocomplete) | office_name=keller |
| license_number | Exact license number | license_number=00007012 |
| license_status | Exact status (vocab varies by state — see /v1/stats) | license_status=Active |
| member_type | Exact member type | member_type=Broker |
| city | Exact city | city=Austin |
| postal_code | Exact postal code (alias: zip) | postal_code=94563 |
| issued_after | ISO date: license issued on/after | issued_after=2024-01-01 |
| issued_before | ISO date: license issued on/before | issued_before=2026-01-01 |
| expires_after | ISO date: license expires on/after | expires_after=2026-08-01 |
| expires_before | ISO date: license expires on/before | expires_before=2026-12-31 |
| first_issued_after | ISO date | first_issued_after=2010-01-01 |
| first_issued_before | ISO date | first_issued_before=2020-01-01 |
| sort | Comma-separated sort columns (whitelisted) | sort=member_last_name,member_first_name |
| order | asc | desc | order=desc |
| page | 1-based page number | page=2 |
| limit | Rows per page (default 100, max 1000) | limit=500 |
| fields | Sparse fieldset: comma-separated column names | fields=id,state,member_full_name |
| count | Set false to skip the total-count query — faster; total/total_pages become null and has_next is inferred from page fill | count=false |
| format | Response format: json (default), csv, or ndjson. For csv/ndjson, pagination is returned in X-Total-Count / X-Page / X-Has-Next response headers; csv also sends a Content-Disposition download filename | format=csv |

Notes:
- Pagination: `page` (1-based) × `limit` (default 100, max 1000); depth
  capped at 50,000 rows — add filters for deeper slices.
- `sort` whitelist: 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.
- `fields` accepts any selectable column (see OpenAPI). Key columns:
  id, snapshot, country, state, member_full_name, member_first_name,
  member_last_name, member_type, member_city, member_state_or_province,
  member_postal_code, office_name, office_broker_name,
  member_state_license_number, member_license_status,
  member_license_issued, member_license_expires,
  member_license_first_issued, member_email, member_preferred_phone,
  matched_email, matched_phone, agent_count.
- `license_status` / `member_type` vocabularies vary by state
  ("Active", "ACTIVE", "Current", "Licensed", …) and match exactly —
  read the real values from `/v1/stats` first.
- All dates are ISO `YYYY-MM-DD`.

## Response shape

```json
{
  "data": [ { "id": 123, "state": "CA", "member_full_name": "…" } ],
  "pagination": { "page": 1, "limit": 100, "total": 12345,
                  "total_pages": 124, "has_next": true, "has_prev": false },
  "meta": { "snapshot": "2026-07", "sort": ["state"], "order": "asc",
            "filters_applied": ["state"] }
}
```

Errors: `{ "error": { "code": "...", "message": "..." } }` with HTTP
400 (invalid parameter), 401 (`unauthorized` — missing/invalid/revoked/
expired key; `key_suspended` — paused by CompCurve, contact us),
403 (`scope_denied` — outside your key's geo/field grant; `history_denied`
— snapshot outside your key's history scope), 404 (not found),
429 (`rate_limited` per-minute/day; `quota_exceeded` monthly quota, resets
on the 1st UTC; `trial_limit`), 5xx (server).

## Recipes

Active brokers in Texas, newest first:

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=TX&member_type=Broker&sort=member_license_issued&order=desc&limit=25" \
  -H "Authorization: Bearer $KEY"
```

Licenses expiring in the next 90 days, with matched emails:

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=FL&expires_after=2026-07-12&expires_before=2026-10-10&fields=id,member_full_name,office_name,member_license_expires,matched_email&limit=200" \
  -H "Authorization: Bearer $KEY"
```

Sparse fields for small payloads:

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=CA,TX,FL&fields=id,state,member_full_name,member_state_license_number&limit=500" \
  -H "Authorization: Bearer $KEY"
```

Historical comparison — same query against an older snapshot (history add-on):

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=CA&snapshot=2026-05&limit=5" \
  -H "Authorization: Bearer $KEY"
```

Track one license month over month — status flips, office moves, expiry
pushes (history add-on):

```sh
curl -s "https://api.compcurve.com/v1/licenses/history?state=TX&license_number=753285-SA&fields=member_full_name,office_name,member_license_status,member_license_expires" \
  -H "Authorization: Bearer $KEY"
# → data.rows (one per snapshot, newest first), data.changes
#   [{ "from_snapshot": "2026-07", "to_snapshot": "2026-08", "field": "office_name", "from": "…", "to": "…" }]
```

Download a filtered slice as CSV (opens in Excel/Sheets; pagination is in the
X-Total-Count / X-Has-Next response headers, and cells starting with =,+,-,@
are prefixed with an apostrophe to block spreadsheet formula injection):

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=TX&license_status=Active&fields=member_full_name,office_name,matched_email,member_license_expires&limit=1000&format=csv" \
  -H "Authorization: Bearer $KEY" -o tx_active.csv
```

Stream as NDJSON (one JSON object per line — pipe-friendly for jq / data tools):

```sh
curl -s "https://api.compcurve.com/v1/licenses?state=FL&format=ndjson&limit=1000" \
  -H "Authorization: Bearer $KEY" | jq -c 'select(.matched_email != null)'
```
