Docs

Check a contractor in two requests.

Base URL: https://api.bettervetters.com. Everything below is JSON over HTTPS. Prefer to click around? Open the interactive API reference.

Quickstart

From zero to a verdict.

1. Create an API key

A key includes 20 free checks and no card is needed. The key is shown once, so store it.

bash
curl -s -X POST https://api.bettervetters.com/v1/keys \
  -H 'content-type: application/json' \
  -d '{"label":"my-agent"}'
json
{
  "api_key": "<shown once>",
  "note": "Store this now; it cannot be shown again.",
  "starter_credit_usd": 3.0,
  "price_per_check_usd": 0.15,
  "checks_included": 20
}

Key creation is limited to 5 keys per IP address per day. Send the key as Authorization: Bearer <key> (or X-API-Key: <key>).

2. Check a contractor

name is the only required field. Add state whenever you know it, and any of city, license_number, phone, or website to raise match confidence.

bash
export BV_API_KEY="<your key>"

curl -s -X POST https://api.bettervetters.com/v1/contractor/check \
  -H "Authorization: Bearer $BV_API_KEY" \
  -H 'content-type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"Example Roofing LLC","state":"WA","city":"Seattle","phone":"206-555-0100"}'

The response is the object shown on the home page. Start with verdict, trust_score, match.confidence, and reasons.

3. Read coverage and your balance

bash
# Coverage: sources, record counts, freshness. Free, no key.
curl -s https://api.bettervetters.com/v1/coverage

# Balance and recent ledger entries
curl -s https://api.bettervetters.com/v1/credits -H "Authorization: Bearer $BV_API_KEY"

Python

python
import os, uuid, httpx

r = httpx.post(
    "https://api.bettervetters.com/v1/contractor/check",
    headers={"Authorization": f"Bearer {os.environ['BV_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())},
    json={"name": "Example Roofing LLC", "state": "WA", "city": "Seattle"},
    timeout=30,
)
check = r.json()
print(check["verdict"], check["trust_score"], check["match"]["confidence"])

Request

POST /v1/contractor/check

FieldTypeNotes
namestring, requiredBusiness or contractor name as you know it. 2 to 200 characters.
statestringTwo-letter US state. Covered: WA, TX, NY. Narrows the search; strongly recommended. Any other state returns out_of_coverage.
citystringUp to 80 characters. A secondary matching signal.
license_numberstringUp to 40 characters. An exact hit gives high confidence.
phonestringUp to 30 characters. Matched against the license record.
websitestringUp to 200 characters. Its domain is compared to the matched name. The site is never fetched.

Headers: Authorization (or X-API-Key), and optionally Idempotency-Key. Retrying with the same idempotency key returns the stored response and is not charged again.

Response

What comes back.

FieldMeaning
statusmatched, not_found, permits_only, or out_of_coverage.
verdicttrusted, likely_ok, caution, high_risk, unknown, not_found, or out_of_coverage.
trust_score0 to 100, or null when there is no reliable match.
matchconfidence (0 to 1), name_score, basis (which signals matched), ambiguous, matched_business_name, and alternates (or closest_candidates when nothing matched).
licenselicense_number, type, class, status, expires, days_until_expiry, suspended_on, verify_url, and more. other_licenses lists related licenses.
insurance_bondWashington only: insurance and bond on file, amounts, providers, expiry.
permitstotal, last_12_months, last_24_months, last_permit_date, jurisdictions, match_basis, and samples with links to the city record.
disciplineavailable, scope, a summary, and actions where the source publishes them.
reasonsList of {code, effect, points, detail} explaining the score.
data_freshnessPer source: last_refreshed_at, publisher_updated_at, refresh_cadence, license, url.
billingcharged_usd, balance_usd, checks_remaining, idempotent_replay.

Treat match.confidence below 0.8 as a possible match and confirm the returned license number or address. A name alone never scores above 0.95. Always cite data_freshness when you show a result to a person.

Scoring

How the trust score and verdict are built.

The score is a weighted sum, normalized to 100 over only the components that can speak to this contractor.

ComponentWeightNotes
License status and expiry50Active scores full; expiring within 60 days scores less; expired, suspended, or revoked score little or none.
Insurance and bond12Washington only. Insurance 8, bond 4.
Discipline13WA suspension on record, or NYC DCWP guilty findings in the last 3 years.
Permit activity25Only counted when permits are found. Zero permits is neutral, because permit coverage is one city per state.

Caps: a suspended or revoked license caps the score at 15, an expired license at 45, and an ambiguous match or confidence below 0.8 at 70. Thresholds: trusted is 80 or more with confidence of 0.8 or more; likely_ok 60 to 79; caution 35 to 59; high_risk is a suspended or revoked license, or under 35.

VerdictWhat it means
trustedConfident match (confidence 0.8+) with a score of 80 or more. License in good standing in the covered sources.
likely_okMostly clean, score 60 to 79. Worth a glance, not a blocker.
cautionNeeds a human look, score 35 to 59. For example an expired license or enforcement findings.
high_riskLicense suspended or revoked, or a score under 35 in the covered sources.
unknownNo reliable match to score, so no judgment either way.
not_foundNo matching record in the covered sources. A missing record is not proof of a problem.
out_of_coverageThe contractor’s state is outside current coverage (WA, TX, NY). Not charged.

Errors

Calm, specific, and actionable.

StatusWhenBody
401Missing or invalid API key.{"error": "missing_api_key"} or {"error": "invalid_api_key"}
402Not enough credits for a check.Payment-required document with price_usd, balance_usd, and top_up. Nothing is charged.
422Request failed validation, for example a state that is not two letters.FastAPI-style detail list.
429More than 5 key creations from one IP in a day.{"error": "rate_limited"}
503Credit packs are coming soon, so checkout is paused.{"error": "checkout_unavailable"}

Out of credits (402)

402 Payment Required
{
  "x402Version": 1,
  "error": "payment_required: insufficient credits",
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "150000",
      "resource": "https://api.bettervetters.com/v1/contractor/check",
      "description": "One contractor trust check (licence, permits, trust score)"
    }
  ],
  "x402_enabled": false,
  "note": "Credit packs are coming soon. Your free starter credits work now.",
  "price_usd": 0.15,
  "balance_usd": 0.0,
  "top_up": {
    "stripe_checkout": "https://api.bettervetters.com/v1/credits/checkout",
    "packs": {
      "pack_10": "$10 credit pack",
      "pack_50": "$50 credit pack (+10%)",
      "pack_200": "$200 credit pack (+20%)"
    },
    "stripe_enabled": false,
    "checkout_enabled": false,
    "message": "Credit packs are coming soon. Your free starter credits work now."
  }
}

The body follows the x402 payment-required format for agent clients, but on-chain settlement is not enabled. Credit packs are coming soon. Your free starter credits work now.

Billing

Credits and packs.

Each billable check draws $0.15 from the key’s credit. Pricing lists what is and is not charged, including the $10, $50, and $200 packs. Credit packs are coming soon. Card checkout is not open yet; POST /v1/credits/checkout returns HTTP 503:

json
{
  "error": "checkout_unavailable",
  "message": "Credit packs are coming soon. Your free starter credits work now."
}

Pack ids will be pack_10, pack_50, and pack_200; they are already listed under pricing.credit_packs in GET /v1/coverage. Your free starter credits work now. Free starter credits have no cash value. Credits do not expire while the account is active. All credit purchases are final and non-refundable, except where applicable law requires a refund, or to correct a duplicate or erroneous charge.

MCP

Use it from an MCP client.

A small stdio MCP server exposes two tools and calls the HTTP API with your key, so billing applies as usual.

ToolWhat it does
check_contractorArguments: name (required), state, city, license_number, phone, website. Returns the same JSON as POST /v1/contractor/check and sends a fresh idempotency key with each call.
get_coverageReturns coverage: jurisdictions, sources, record counts, and freshness. Free.

Setup

  1. Create an API key (see the quickstart).
  2. Create a Python 3.11+ virtualenv and install the dependencies: pip install "mcp>=1.2" "httpx>=0.27".
  3. Download server.py and note its path. Full setup notes are in the MCP README.
  4. Add the server to your MCP client’s configuration, using the paths from your machine:
mcp config
{
  "mcpServers": {
    "better-vetters": {
      "command": "python",
      "args": [
        "/path/to/server.py"
      ],
      "env": {
        "CONTRACTOR_CHECK_API_URL": "https://api.bettervetters.com",
        "CONTRACTOR_CHECK_API_KEY": "bv_live_YOUR_KEY"
      }
    }
  }
}

Set command to the Python that has the dependencies installed (for example your virtualenv’s python). The server also accepts BETTER_VETTERS_API_URL and BETTER_VETTERS_API_KEY as aliases for the two variables above.

Ask your agent something like “Check whether Example Roofing LLC in Seattle, WA is licensed.” It calls check_contractor and gets the verdict, score, and records back.

Machine-readable files

For agents and tooling.

Run your first 20 checks free.

Create a key and send one request.

create a key
# Get a key (20 free checks, no card). The key is shown once.
curl -s -X POST https://api.bettervetters.com/v1/keys \
  -H 'content-type: application/json' \
  -d '{"label":"my-agent"}'