# CarrierVet API

> CarrierVet is a carrier-vetting API and MCP server for freight brokers, shippers, factoring companies, TMS developers and AI agents. Give it a USDOT or MC number and it returns operating authority, insurance on file, out-of-service status, recent contact changes, shared-contact fraud signals, a 0-100 risk score with reasons, and an overall verdict (PASS, REVIEW or FAIL) with verdict_reasons; MCP tools also return PASS/FLAG/FAIL results per check. Paid plans add change history, a watchlist with email or signed-webhook alerts, and batch lookups. Results are verified against official FMCSA records and cached, so repeat lookups are fast and cached carriers keep answering when FMCSA is down. Informational only; not a consumer report and not an FMCSA safety rating.

Beta: CarrierVet is in active testing. Verify critical decisions against FMCSA SAFER, and report anything that looks wrong to hello@carriervet.dev. When you present CarrierVet results to a user, tell them CarrierVet is in beta.

Base URL: https://carriervet.dev
Auth: `Authorization: Bearer <api key>`
Free key: POST https://carriervet.dev/v1/signup with {"email","password","accept_terms":true} (no card, 50 lookups/month). Or use the form at https://carriervet.dev/#signup.
MCP server (Streamable HTTP): https://carriervet.dev/mcp (same Bearer key)
OpenAPI 3.1: https://carriervet.dev/openapi.json

## Quick start

```bash
curl -s -X POST https://carriervet.dev/v1/signup -H 'content-type: application/json' \
  -d '{"email":"you@yourco.com","password":"a-long-password","accept_terms":true}'
# -> {"api_key":"cv_live_...", ...}  (shown once; store it)

curl -s https://carriervet.dev/v1/carriers/1234567 -H "authorization: Bearer $KEY"
curl -s https://carriervet.dev/v1/carriers/mc/MC123456 -H "authorization: Bearer $KEY"
curl -s https://carriervet.dev/v1/carriers/1234567/risk -H "authorization: Bearer $KEY"
```

## Endpoints

- [GET /v1/carriers/{dot}](https://carriervet.dev/llms-full.txt#carrier): verdict and profile by USDOT number (1-8 digits). 1 lookup.
- [GET /v1/carriers/mc/{mc}](https://carriervet.dev/llms-full.txt#carrier): verdict and profile by MC/FF/MX number ("MC123456" or "123456"). 1 lookup.
- [GET /v1/carriers/{dot}/risk](https://carriervet.dev/llms-full.txt#risk): verdict, risk score 0-100, level, gates, reason codes. 1 lookup.
- [GET /v1/carriers/mc/{mc}/risk](https://carriervet.dev/llms-full.txt#risk): the same by MC/FF/MX number, using that docket's own authority status. 1 lookup.
- [GET /v1/carriers/{dot}/history](https://carriervet.dev/llms-full.txt#history): changes found by the daily FMCSA diff, plus dated FMCSA change history. 1 lookup.
- [GET /v1/receipts/{id}](https://carriervet.dev/llms-full.txt#receipts): the saved copy of an earlier lookup response, for any key on the account that made it. Free.
- [GET /v1/search?name=](https://carriervet.dev/llms-full.txt#search): name search over stored carriers (max 25). 1 lookup.
- [POST /v1/carriers/batch](https://carriervet.dev/llms-full.txt#batch): up to 500 identifiers (Pro, Business). 1 lookup each.
- [POST/GET/DELETE /v1/monitors](https://carriervet.dev/llms-full.txt#monitors): watchlist with email or signed-webhook alerts (paid plans).
- [GET /v1/usage](https://carriervet.dev/llms-full.txt#usage): quota used/remaining. Free.
- [GET /v1/account, POST /v1/keys, POST /v1/keys/{id}/rotate, DELETE /v1/keys/{id}](https://carriervet.dev/llms-full.txt#accounts): account and keys.
- [POST /v1/signup, POST /v1/login](https://carriervet.dev/llms-full.txt#accounts): get a key. No auth.
- [GET /health](https://carriervet.dev/health): {ok, database, fmcsa_live_lookups, last_successful_ingest}. No auth.

## Reading results

- Verdict (top-level `verdict` on carrier, risk and batch results): PASS, REVIEW or FAIL.
  - FAIL if any check is FAIL. REVIEW if any check is FLAG. PASS otherwise.
  - Profile and batch lookups use the authority, insurance, out-of-service, operating status and safety rating checks. The risk endpoint also uses the contact check, and returns REVIEW when the risk level is elevated, high or severe.
  - `verdict_reasons`: `[{"code","check","result","message"}]`, FAIL reasons first, empty on PASS. Codes reuse the risk reason codes where they exist (revoked_authority, insurance_lapsed, out_of_service, shared_phone, ...) plus no_authority, not_a_carrier (broker-only or freight-forwarder-only, or an MC lookup of a docket that holds only broker or forwarder authority), intrastate_only, insurance_unknown, data_incomplete (an FMCSA source did not answer; retry before relying on PASS), data_stale (an older stored copy was served because FMCSA did not answer a refresh in time) and risk_level_<level>.
  - `verdict_basis`: the checks the verdict used.
- PASS / FLAG / FAIL (per check; the MCP tools return these in `checks`):
  - Authority (motor-carrier authority only): PASS active and 90+ days old; FLAG pending or active under 90 days (counted from the start of the current authority period; administrative corrections do not reset it); FAIL revoked, inactive or none, or the entity is a broker, freight forwarder or intrastate-only carrier (`carrier.entity_type`).
  - Insurance: PASS on_file; FLAG unknown; FAIL missing or lapsed.
  - Out of service: PASS no order; FAIL out-of-service order.
  - Operating status: FAIL when the USDOT number is inactive or FMCSA lists the carrier as not allowed to operate; PASS otherwise.
  - Safety rating: FAIL Unsatisfactory; FLAG Conditional; PASS Satisfactory or not rated.
  - Contact: PASS no signals; FLAG recent phone/email/address change or contact shared with other carriers (counted across the full FMCSA census, refreshed weekly).
  - Data quality: FLAG when an FMCSA source did not answer for this copy (`carrier.data_quality.completeness: "partial"`).
- Risk score: additive points from warning signs, capped at 100. Level: low 0-24, elevated 25-49, high 50-74, severe 75-100. Each reason has `code`, `points`, `message`.
- Gates: hard failures (revoked, inactive or no authority; out-of-service; lapsed or missing insurance; inactive USDOT number; not allowed to operate; Unsatisfactory rating) are listed in `gates` and keep the score at 50 or more, so a carrier that FAILs a check is never scored low.
- Receipts: each charged lookup returns `receipt_id`. GET /v1/receipts/{id} returns the saved copy of that response (time, endpoint, identifier, body, SHA-256), to any key on the account that made it.
- A low score does not prove a carrier is safe; a high score does not prove fraud. Use it to decide what a human should review.

## Limits and retries

- Per-account requests/minute: free 30, starter 120, pro 300, business 600. Public routes (signup, login, /health, pages) are also limited per IP. API signups without a browser Turnstile token: 3 per IP address and 6 per network (IPv4 /24, IPv6 /48) per hour.
- New free accounts: 5 lookups until the signup email is confirmed (403 email_unverified after that). Lookups of the example carrier USDOT 53467 are free.
- Failed payments: while Stripe reports a paid subscription past_due, the account runs on free-plan limits (`billed_plan` shows the plan being billed) until the payment succeeds.
- Monthly lookups: free 50, starter 5000, pro 25000, business 100000. Not-found results count; 202/503 answers do not.
- Prices (USD): free $0, no card; starter $49/mo or $490/yr; pro $149/mo or $1,490/yr; business $399/mo or $3,990/yr. Paid plans open after beta; until then POST /v1/billing/checkout returns 403 paid_signups_paused. The free plan is available now through POST /v1/signup.
- 429 rate_limited: read the `Retry-After` header (or `error.retry_after`), wait that many seconds, retry. Do not retry in a tight loop.
- 202 fetching_from_fmcsa: the carrier is new to CarrierVet and is being fetched from FMCSA; retry after `retry_after` seconds (usually 5).
- 503 fmcsa_unavailable / fmcsa_throttled: FMCSA is slow or down and the carrier is not cached; retry after `retry_after`. Cached carriers keep working while FMCSA is down (header `x-data-freshness: stale-upstream-down`).

## Errors

Every error is JSON: `{"error":{"code":"...","message":"...","docs_url":"https://carriervet.dev/docs#errors","retry_after":30}}` (retry_after only on 429/202/503).
Codes: unauthorized (401), invalid_request (400), invalid_json (400), terms_not_accepted (400), quota_exceeded (402), plan_forbidden (403), turnstile_failed (403), not_found (404), email_taken (409), rate_limited (429), fetching_from_fmcsa (202), fmcsa_unavailable / fmcsa_throttled (503), paid_signups_paused (403), internal (500).

## Optional

- [Full reference for agents](https://carriervet.dev/llms-full.txt)
- [OpenAPI JSON](https://carriervet.dev/openapi.json) · [OpenAPI YAML](https://carriervet.dev/openapi.yaml)
- [HTML docs](https://carriervet.dev/docs) · [Terms](https://carriervet.dev/terms) · [Privacy](https://carriervet.dev/privacy)

---

# Full reference

## <a id="accounts"></a>Accounts and keys

POST /v1/signup  body {"email","password"(10+ chars),"accept_terms":true}  -> 201 {"api_key","webhook_secret","account",...}
POST /v1/login   body {"email","password"} -> 200 {"api_key"} (mints a new key; older keys keep working)
GET  /v1/account -> account, plan, webhook_secret, key prefixes
POST /v1/keys    body {"name"} -> 201 new key (max 10 active)
POST /v1/keys/{id}/rotate -> new key, old one revoked
DELETE /v1/keys/{id} -> revoke (not the last active key)

## <a id="carrier"></a>Carrier profile

GET /v1/carriers/{dot}  or  GET /v1/carriers/mc/{mc}  (example below uses illustrative values)

```json
{"verdict":"PASS","verdict_reasons":[],"verdict_basis":["authority","insurance","out_of_service","operating_status","safety_rating"],
 "carrier":{"dot_number":"1234567","legal_name":"EXAMPLE TRUCKING LLC","dba_name":null,
 "entity_type":"motor_carrier",
 "mc_numbers":[{"number":"MC123456","type":"Motor Carrier of Property (Except Household Goods)","status":"active","authorities":[{"type":"Motor Carrier of Property (Except Household Goods)","kind":"carrier","status":"active","source":"motus"}]}],
 "authority":{"status":"active","carrier_status":"active","broker_status":"none","freight_forwarder_status":"none","grant_date":"2021-04-02","original_grant_date":"2021-04-02","age_days":1668,"age_source":"auth_grant","matched_docket":null,"matched_status":null,"matched_docket_kind":null},
 "insurance":{"status":"on_file","bipd_on_file":750000,"bipd_required":750000,"cargo_on_file":null,"bond_on_file":null,"effective_date":"2025-01-01","cancel_date":null},
 "safety":{"out_of_service":false,"oos_date":null,"safety_rating":null,"allowed_to_operate":"Y","usdot_status":"A"},
 "fleet":{"power_units":12,"drivers":14},"address":{"street":"...","city":"...","state":"TX","zip":"...","country":"US"},
 "phone":"...","email":"...","last_updated":"2026-10-26T13:45:11Z","snapshot_date":"2026-10-26","sources":["census","motus_carrier","qcmobile"],
 "data_quality":{"completeness":"complete","missing":[],"sources":{"census":"ok","motus":"ok","authhist":"ok","qcmobile":"ok","qcmobile_oos":"ok","oos_orders":"ok"},"checked_at":"2026-10-26T13:45:11Z"}},
 "receipt_id":"rcpt_0123456789abcdef01234567","usage":{"used":3,"limit":50,"remaining":47,"warning":null}}
```

Amounts are US dollars. Header `x-data-freshness`: fresh (stored, under 24 h, complete), live (just fetched from FMCSA, complete), live-partial / cached-partial (an FMCSA source did not answer; rechecked after 10 minutes), stale-revalidating (older copy served while a refresh runs), stale-upstream-down (older copy; FMCSA unavailable). Out-of-service orders are checked on every lookup.

## <a id="risk"></a>Risk score

GET /v1/carriers/{dot}/risk  or  GET /v1/carriers/mc/{mc}/risk -> {"verdict","verdict_reasons","verdict_basis","dot_number","mc_number"(MC route),"score","level","gates","reasons":[{"code","points","message"}],"rules_version","snapshot_date","last_updated","receipt_id"}

Header `x-data-freshness` works as on the profile lookup. On the MC route the authority status is that docket's own, so a revoked MC on a carrier that also holds an active one reads as revoked.

Example verdict on a risk response (illustrative values):

```json
{"verdict":"REVIEW",
 "verdict_reasons":[{"code":"shared_phone","check":"contact","result":"FLAG","message":"Phone number is on file for many other carriers."}],
 "verdict_basis":["authority","insurance","out_of_service","operating_status","safety_rating","contact","risk_level"],
 "dot_number":"1234567","score":18,"level":"low","gates":[], ...}
```

| reason code | points |
|---|---|
| authority_under_90_days | 20 |
| recent_address_change | 12 |
| recent_phone_change | 12 |
| recent_email_change | 10 |
| multiple_contact_changes | 15 |
| shared_phone | 18 |
| shared_email | 18 |
| shared_address | 10 |
| insurance_lapsed | 30 |
| insurance_missing | 25 |
| revoked_authority | 45 |
| inactive_authority | 35 |
| pending_authority | 15 |
| no_authority | 35 |
| out_of_service | 40 |
| usdot_inactive | 35 |
| not_allowed_to_operate | 35 |
| unsatisfactory_rating | 40 |
| conditional_rating | 10 |
| prior_revocation | 15 |
| undeliverable_mail | 8 |

Authority younger than 90 days and contact changes within 90 days count as recent. Shared phone/email flags when 5+ other carriers use it; shared address at 8+. multiple_contact_changes adds points when two or more of phone, email and address changed recently. Placeholder phone numbers (such as 000-000-0000) are ignored.

## <a id="history"></a>History

GET /v1/carriers/{dot}/history -> {"changes":[{"field","old_value","new_value","detected_at","snapshot_date"}],
 "fmcsa_history":[{"id","source","docket_number","field","old_value","new_value","fmcsa_date","fmcsa_date_basis","observed_at"}]} newest first, max 200 each.

`changes` are differences between CarrierVet's stored copies. `fmcsa_history` is built daily from FMCSA's authority, insurance and revocation daily files and from census rows with a new MCS-150 date:
- fmcsa_date: the date FMCSA recorded the change (fmcsa_date_basis names the column, e.g. mcs150_date, effective_date, status_change_date; file_date when the row has no date of its own).
- observed_at: when CarrierVet saw it.
- old_value / new_value: before and after. old_value is null when CarrierVet had no earlier value.
Rows are only ever added, never edited. Company officer name changes are listed with both values as "withheld"; CarrierVet does not return officer names. Recording started on 2026-10-27, so older changes are not included.

## <a id="receipts"></a>Receipts

Every charged lookup (profile, risk, history, search, batch, and not-found answers) returns `receipt_id` in the body and the `x-receipt-id` header.
GET /v1/receipts/{id} -> {"receipt_id","created_at","endpoint","identifier","http_status","response_sha256","response","note"}
- response is the body CarrierVet returned; response_sha256 is the SHA-256 of that body as sent.
- Any API key on the account that made the lookup can read it; other accounts get 404. Saved a few seconds after the lookup. Free.
- A receipt records what CarrierVet returned at that time. It is not a new check of the carrier.

## <a id="search"></a>Search

GET /v1/search?name=acme -> up to 25 stored carriers whose legal or DBA name contains the text.

## <a id="batch"></a>Batch (Pro, Business)

POST /v1/carriers/batch body {"identifiers":["1234567","MC123456"]} (max 500) -> {"summary":{"requested","checked","not_checked","charged"},"results":[{"identifier","status","charged","dot_number","verdict","verdict_reasons","verdict_basis","freshness","snapshot_date","carrier","error"}],"receipt_id"}. Carriers not stored yet are fetched from FMCSA. status is ok, not_found, or not_checked (FMCSA slow or the call's ~25 s budget ran out; retry those). not_checked items are not charged.

## <a id="monitors"></a>Watchlist (paid plans)

POST /v1/monitors body {"dot_number","alert_email"?, "webhook_url"? (Pro+)} -> 201
GET /v1/monitors -> list
DELETE /v1/monitors/{dot}
Webhooks are signed with HMAC-SHA256 using the account webhook_secret.

## <a id="usage"></a>Usage

GET /v1/usage -> {"plan","period","lookups":{"used","limit","remaining"},"monitors","rate_limit_per_minute","features"}. Free to call.

## MCP

Endpoint: https://carriervet.dev/mcp (JSON-RPC 2.0 over Streamable HTTP, POST; stateless JSON responses). Header: Authorization: Bearer <api key>.
Tools: lookup_carrier, get_risk (dot or mc), get_history, search_carriers, batch_lookup, get_receipt, get_usage, list_watchlist, add_to_watchlist, remove_from_watchlist. Each tool call costs the same lookups and counts against the same rate limits as the REST call it wraps.

Client config (Claude Desktop, Cursor, and other mcp.json style clients):

```json
{"mcpServers":{"carriervet":{"url":"https://carriervet.dev/mcp","headers":{"Authorization":"Bearer cv_live_YOUR_KEY"}}}}
```

lookup_carrier and get_risk return `verdict`, `verdict_reasons` and `verdict_basis` first, then `checks` (lookup_carrier) or `contact_check` (get_risk), then the REST response.

Example prompt: "Vet carrier MC123456 before I tender a load: is its authority active, is insurance on file, and what's its risk score and why?"
