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.
curl -s -X POST https://api.bettervetters.com/v1/keys \
-H 'content-type: application/json' \
-d '{"label":"my-agent"}'{
"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.
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
# 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
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
| Field | Type | Notes |
|---|---|---|
name | string, required | Business or contractor name as you know it. 2 to 200 characters. |
state | string | Two-letter US state. Covered: WA, TX, NY. Narrows the search; strongly recommended. Any other state returns out_of_coverage. |
city | string | Up to 80 characters. A secondary matching signal. |
license_number | string | Up to 40 characters. An exact hit gives high confidence. |
phone | string | Up to 30 characters. Matched against the license record. |
website | string | Up 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.
| Field | Meaning |
|---|---|
status | matched, not_found, permits_only, or out_of_coverage. |
verdict | trusted, likely_ok, caution, high_risk, unknown, not_found, or out_of_coverage. |
trust_score | 0 to 100, or null when there is no reliable match. |
match | confidence (0 to 1), name_score, basis (which signals matched), ambiguous, matched_business_name, and alternates (or closest_candidates when nothing matched). |
license | license_number, type, class, status, expires, days_until_expiry, suspended_on, verify_url, and more. other_licenses lists related licenses. |
insurance_bond | Washington only: insurance and bond on file, amounts, providers, expiry. |
permits | total, last_12_months, last_24_months, last_permit_date, jurisdictions, match_basis, and samples with links to the city record. |
discipline | available, scope, a summary, and actions where the source publishes them. |
reasons | List of {code, effect, points, detail} explaining the score. |
data_freshness | Per source: last_refreshed_at, publisher_updated_at, refresh_cadence, license, url. |
billing | charged_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.
| Component | Weight | Notes |
|---|---|---|
| License status and expiry | 50 | Active scores full; expiring within 60 days scores less; expired, suspended, or revoked score little or none. |
| Insurance and bond | 12 | Washington only. Insurance 8, bond 4. |
| Discipline | 13 | WA suspension on record, or NYC DCWP guilty findings in the last 3 years. |
| Permit activity | 25 | Only 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.
| Verdict | What it means |
|---|---|
| trusted | Confident match (confidence 0.8+) with a score of 80 or more. License in good standing in the covered sources. |
| likely_ok | Mostly clean, score 60 to 79. Worth a glance, not a blocker. |
| caution | Needs a human look, score 35 to 59. For example an expired license or enforcement findings. |
| high_risk | License suspended or revoked, or a score under 35 in the covered sources. |
| unknown | No reliable match to score, so no judgment either way. |
| not_found | No matching record in the covered sources. A missing record is not proof of a problem. |
| out_of_coverage | The contractor’s state is outside current coverage (WA, TX, NY). Not charged. |
Errors
Calm, specific, and actionable.
| Status | When | Body |
|---|---|---|
401 | Missing or invalid API key. | {"error": "missing_api_key"} or {"error": "invalid_api_key"} |
402 | Not enough credits for a check. | Payment-required document with price_usd, balance_usd, and top_up. Nothing is charged. |
422 | Request failed validation, for example a state that is not two letters. | FastAPI-style detail list. |
429 | More than 5 key creations from one IP in a day. | {"error": "rate_limited"} |
503 | Credit packs are coming soon, so checkout is paused. | {"error": "checkout_unavailable"} |
Out of credits (402)
{
"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:
{
"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.
| Tool | What it does |
|---|---|
check_contractor | Arguments: 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_coverage | Returns coverage: jurisdictions, sources, record counts, and freshness. Free. |
Setup
- Create an API key (see the quickstart).
- Create a Python 3.11+ virtualenv and install the dependencies:
pip install "mcp>=1.2" "httpx>=0.27". - Download
server.pyand note its path. Full setup notes are in the MCP README. - Add the server to your MCP client’s configuration, using the paths from your machine:
{
"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.
https://api.bettervetters.com/openapi.jsonOpenAPI 3.1 spechttps://api.bettervetters.com/llms.txtAPI summary for LLMshttps://bettervetters.com/llms.txtSite summary for LLMshttps://api.bettervetters.com/v1/coverageLive coverage, free, no key
Run your first 20 checks free.
Create a key and send one request.
# 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"}'