tradesapi
Sign in Dashboard

TradesAPI reference.

One normalized API for contractor license verification across all 50 states + DC, plus 9 municipal licensing boards. Send a license number, state, and trade — get back validity, expiration, status, and disciplinary history in the same shape everywhere.

Introduction.

The API is served from https://www.tradesapi.com. All endpoints return JSON. The core primitive is a verification: a lookup of one license against the authoritative state (or city) licensing source, normalized into a single response schema.

  • Coverage — all 50 states + DC at the state level, plus city boards in Chicago, New York City, Philadelphia, Detroit, Atlanta, Dallas, Nashville, Las Vegas, and Jacksonville.
  • Freshness you can audit — every response carries checked_at and cached. Active results may be served from cache for up to 24 hours; expired, not-found, and error results are always re-verified. Pass fresh=true to force a live check.
  • Credits — one credit per successful verification, two per name search. Failed lookups and unsupported jurisdictions are not charged. New keys include 50 free verifications.

Quickstart.

Sign in at /auth/login (Google or email code), copy your API key from the dashboard, and verify your first license:

curl "https://www.tradesapi.com/verify?state=TX&license=TACLA00000103C&trade=hvac" \
  -H "X-API-Key: YOUR_KEY"

Want to wire up CI first? The sandbox serves deterministic fixture responses on the same endpoints at zero credit cost.

Authentication.

Every authenticated endpoint takes your key in the X-API-Key request header. Keys are shown once at creation and stored hashed — treat them like passwords and rotate from the dashboard if one leaks.

Two key scopes matter for integration work:

  • user — production keys. Verifications charge credits and hit live sources.
  • sandbox — test keys. Same endpoints and response schema, deterministic fixtures, zero credit cost. See sandbox testing.

The hosted MCP endpoint additionally supports an OAuth 2.1 browser flow — see MCP server.

Errors.

Errors return a consistent JSON body:

{
  "error": "...",
  "detail": "..."
}
StatusMeaningNotes
400bad requestInvalid or missing parameters, unsupported jurisdiction.
401unauthorizedMissing or invalid X-API-Key.
402payment requiredInsufficient credit balance for the request.
429rate limitedPer-key rate limit exceeded; honor Retry-After.
502upstream failureVerification temporarily unavailable. The detail includes a short correlation reference you can quote to support; internal error detail is never exposed.

Rate limits.

Requests are rate-limited per API key with a Redis sliding window (default 100 requests/minute). Every response carries the standard headers:

  • X-RateLimit-Limit — your per-minute ceiling
  • X-RateLimit-Remaining — requests left in the current window
  • X-RateLimit-Reset — when the window resets

On 429, back off until Retry-After. Sandbox keys share the same limit config, so you can tune request pacing in CI before pointing at production.

Verify a license.

GET/verify

Look up one license by state, license number, and trade. Returns the normalized LicenseResult. One credit per successful verification.

Query parameters

ParameterTypeDescription
statestring, requiredTwo-letter state code, e.g. TX, CA.
licensestring, requiredLicense number as issued by the licensing board.
tradestringTrade type, e.g. hvac, plumbing, electrical. Defaults to general.
citystringOptional municipality slug (e.g. houston, chicago) to target a city licensing board instead of the state. Composes into a jurisdiction like TX_houston.
freshbooleanForce a live check, bypassing the cache and the DB-first read layer. Same one-credit cost. Default false.
license_typestringRoster filter for states that publish parallel rosters (currently Mississippi): commercial or residential. Other states ignore it.

Response

The same shape for every jurisdiction — your integration code doesn't change per state:

{
  "valid": true,
  "name": "SAMPLE HVAC SERVICES LLC",
  "license_number": "TACLA00000103C",
  "trade": "hvac",
  "expiration": "05/12/2027",
  "status": "Active",
  "state": "TX",
  "disciplinary_actions": [],
  "source_url": "https://www.tdlr.texas.gov/LicenseSearch/",
  "cached": false,
  "checked_at": "2026-07-07T15:04:00Z"
}
  • valid — whether the license is currently valid for the trade.
  • status — the board's own status wording (e.g. Active, Expired, Suspended).
  • disciplinary_actions — enforcement actions when the board publishes them, in the same response. No second lookup.
  • cached / checked_at — freshness audit trail for this answer.

Municipal lookups

Some trades are licensed by the city, not the state. Add city to route to the municipal board under the same schema:

curl "https://www.tradesapi.com/verify?state=IL&city=chicago&license=EXAMPLE123&trade=general" \
  -H "X-API-Key: YOUR_KEY"

Batch verification.

POST/batch

Verify up to 200 licenses in one request, run in parallel. Partial failures don't block other items — each result is returned independently, and credits are charged only for successful verifications. For larger workloads, chunk into 200-item calls client-side.

curl -X POST "https://www.tradesapi.com/batch" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "licenses": [
      { "state": "TX", "license": "TACLA00000103C", "trade": "hvac" },
      { "state": "CA", "license": "1012345", "trade": "general" }
    ]
  }'

Results come back in the same order as the request. Credit deduction is atomic — you're never partially charged for a failed item.

Supported jurisdictions.

GET/states

Public, no auth. Returns every supported jurisdiction with integration metadata — one row per state, with municipal boards nested under their parent state:

  • code, name, portal_url
  • supported_trades — trades observed for the jurisdiction (best effort)
  • supports_name_search — whether /search works there
  • statushealthy | degraded | maintenance
  • municipalities — nested city rows, same shape with city in place of name

For live operational status, the status page and /status.json refresh continuously and are safe for uptime monitors.

Credit balance.

GET/billing/balance

Returns your key's current credit balance and cumulative usage:

{
  "credit_balance": 412,
  "usage_count": 88,
  "owner": "you@example.com"
}

Credit packs are purchased from the dashboard; credits never expire.

Monitoring watch lists.

POST/monitor

A /verify call is a snapshot. Monitoring keeps watching: add a license to your watch list and it is re-checked against source-of-truth data on an ongoing basis; when its status, expiration, name, trade, or classifications change, you're notified by webhook or email.

FieldTypeDescription
jurisdictionstring, requiredState code or municipal composite, e.g. TX or TX_houston.
license_numberstring, requiredLicense to watch.
labelstringOptional free-text label (up to 500 chars) — your vendor ID, project name, etc.
notifyobjectOptional: webhook_subscription_ids (IDs of your webhook subscriptions) and/or email.

Returns 201 for a new monitor, 200 on an idempotent re-POST of an existing one, 400 for an uncovered jurisdiction, and 429 when your active-monitor quota is exhausted.

curl -X POST "https://www.tradesapi.com/monitor" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jurisdiction": "TX",
    "license_number": "TACLA00000103C",
    "label": "vendor-1042",
    "notify": { "webhook_subscription_ids": [1] }
  }'

Bulk import.

POST/monitor/bulk

Load a whole roster at once: POST a CSV body (Content-Type: text/csv) with a header row. Recognized columns (case-insensitive): jurisdiction, license_number, optional label. Extra columns are ignored. Up to 5,000 rows per request.

curl -X POST "https://www.tradesapi.com/monitor/bulk" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary $'jurisdiction,license_number,label\nTX,TACLA00000103C,vendor-1042\nCA,1012345,vendor-2210'

Always returns 200 with per-row outcomes (created | existed | error | quota_exceeded) so one bad row never fails the import.

Manage monitors.

GET/monitor GET/monitor/usage GET/monitor/{id} PATCH/monitor/{id} DELETE/monitor/{id}
  • GET /monitor — list your monitors, paginated (limit up to 100).
  • GET /monitor/usage — active-monitor count against your quota, for previewing the bill.
  • GET /monitor/{id} — one monitor's detail.
  • PATCH /monitor/{id} — update label or notify config.
  • DELETE /monitor/{id} — stop watching (returns 204). Re-POSTing the same license later reactivates it.

The whole roster is also manageable from the dashboard.

Webhook subscriptions.

POST/webhooks/subscriptions GET/webhooks/subscriptions PATCH/webhooks/subscriptions/{id} DELETE/webhooks/subscriptions/{id}

Create a subscription with the HTTPS URL you want events delivered to, plus an optional event filter and description. The response includes your signing_secret — shown exactly once; store it to verify deliveries.

curl -X POST "https://www.tradesapi.com/webhooks/subscriptions" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/tradesapi", "description": "prod" }'

Event types use a dotted scheme so you can pattern-match on the license. prefix:

  • license.status_changed
  • license.expiration_changed
  • license.classifications_changed
  • license.name_changed
  • license.trade_changed

Useful extras: POST /webhooks/subscriptions/{id}/test sends a test event to your endpoint, and POST /webhooks/subscriptions/{id}/rotate-secret issues a new secret (the previous one stays valid for a grace window so you can roll over without dropped verifications).

Verifying signatures.

Every delivery is signed. Headers on each POST to your endpoint:

HeaderDescription
X-Tradesapi-Signaturet=<unix-timestamp>,v1=<hex-hmac>
X-Tradesapi-Event-IdStable event ID — deduplicate on this.
X-Tradesapi-Subscription-IdWhich subscription this delivery belongs to.
X-Tradesapi-Delivery-AttemptAttempt number (1 on first try).

To verify: compute HMAC-SHA256(secret, "<t>." + raw_body) and compare the hex digest against v1 in constant time. Sign over the exact raw bytes you received — re-encoding the JSON will invalidate the signature. Reject stale timestamps to prevent replay.

import hashlib, hmac

def verify(header: str, body: bytes, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts["t"], parts["v1"]
    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Deliveries & retries.

GET/webhooks/deliveries

Failed deliveries are retried automatically with backoff. The deliveries feed lists recent attempts with status codes and timings so you can audit what reached your endpoint, and a redeliver action lets you replay a specific delivery after fixing your receiver. Respond with any 2xx quickly (do heavy work async) — anything else counts as a failure and schedules a retry.

MCP server.

TradesAPI speaks Model Context Protocol, so Claude, Cursor, and any MCP-compatible agent can verify licenses directly.

Hosted (recommended)

Point your MCP client at the hosted endpoint — connect with just the URL and complete the OAuth 2.1 browser flow, or pass an API key as a bearer token:

{
  "mcpServers": {
    "tradesapi": {
      "type": "streamable-http",
      "url": "https://www.tradesapi.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}

Local stdio

Prefer to run it locally? The open-source server is on npm:

{
  "mcpServers": {
    "tradesapi": {
      "command": "npx",
      "args": ["-y", "contractor-license-mcp-server"],
      "env": {
        "CLV_API_URL": "https://www.tradesapi.com",
        "CLV_API_KEY": "your-api-key-here"
      }
    }
  }
}

Tools exposed: single verify, batch verify (up to 25 per call), and name search. Source on GitHub; listed in the MCP Registry.

Sandbox testing.

Sandbox keys hit the same endpoints (/verify, /batch, /search) with deterministic fixture responses keyed by license-number prefix (SANDBOX-VALID-*, SANDBOX-EXPIRED-*, SANDBOX-DISCIPLINARY-*, ...). Zero credit cost, safe to assert against in CI, and every response carries "sandbox": true plus an X-Sandbox: true header.

Full sentinel table and examples: sandbox guide.

OpenAPI & interactive explorer.