Getting Started

Sandbox is free. API is $500/mo unlimited.

Download OpenAPI

Get a Sandbox Key

Log in, issue a free key, and use it with any of the 50 fixture carriers.

Make your first request

curl "https://mds.motorcarrier.ai/v2/profile?dot=54283" \
  -H "Authorization: Bearer YOUR_API_KEY"

Authorization: Bearer. Sandbox key hits the 50 fixture carriers. Paid key is unlimited.

Explore the response

Full dossier lives on GET /v2/profile — open Profile for the field-by-field schema.

Sandbox is fixture data for those 50 DOTs on GET /v2/profile and GET /v2/profile-lite, same response shapes, 30 requests/min. Paid is live warehouse, $500/mo unlimited, 60 requests/min. Paid carrier-list, autocomplete, and monitoring access requires an mca_live_ key with the $500/mo API subscription.

Sandbox

Free

Build, prototype, and test before you spend a cent.

  • Fixture data for 50 carriers
  • Warehouse lookups, live response shape
  • No credit card required

API

$500/mo

Unlimited pulls

Capability matrix

When to call which route — always pick the smallest payload that finishes the job.

RouteReturnsUse whenPrefer instead whenAuthSandbox
GET /v2/profileFat dossier groups: identity, authority, iss, flags, inspection_units, contact_history, reliability_signals, risk_index (+ live groups as shipped)Underwriting / deep review / one-carrier truthYou only need name+fleet → lite; browsing many → carriers/autocompleteBearerYes (50)
GET /v2/profile-litecarrier identity/address/fleet + lookups_remainingAfter typeahead / fast enrichmentNeed flags/ISS/inspections → profileBearerYes (50)
GET /v2/carriersLean list + pagination; inferred renewal fieldsDaily lead ingest by units + renewal windowSingle DOT known → profile/litePaid mca_live_No
GET /v2/autocompleteLean results[] hitsTypeahead DOT/MC/name/DBAExact DOT known → lite/profilePaid mca_live_No
GET /v2/monitoringitems[] watches from stored snapshotDashboard of watched DOTsNeed fresh dossier → profilePaid mca_live_No
POST /v2/monitoringitem + createdStart watching a DOTDOT unknown to warehouse → will 404Paid mca_live_No
DELETE /v2/monitoring/{dot}removedStop watching—Paid mca_live_No
GET /v2/monitoring/eventsevents[] + next_cursorPoll for authority/insurance/safety changes on watched DOTsNeed the fresh dossier, not just the diff → profilePaid mca_live_No
POST /v2/monitoring/webhooksid + url + secret (once) + created_atRegister a push endpoint instead of pollingSecret is shown once — store it immediatelyPaid mca_live_No
GET /v2/monitoring/webhookswebhooks[] (no secrets)Audit registered endpointsNeed the secret again → re-registerPaid mca_live_No
DELETE /v2/monitoring/webhooks/{id}revokedStop delivery to an endpoint—Paid mca_live_No
POST /v2/exportsid + stateQueue a filtered CSV export over more rows than carriers/list pagination is worthNeed it right now / under ~500 rows → carriersPaid mca_live_No
GET /v2/exports/{id}state + authenticated download url when readyPoll a queued export—Paid mca_live_No

Auth & limits

Authorization

ItemDetail
HeaderAuthorization: Bearer YOUR_API_KEY
Sandbox keyFixture carriers only (50 DOTs) on profile + profile-lite
Paid keyLive warehouse; $500/mo unlimited pulls
Paid-only routescarriers, autocomplete, monitoring family, exports family — require mca_live_ + monthly API subscription
Owner bindingMonitoring and exports fail closed with 403 if paid key has no owner (cannot resolve watchlist / export ownership)
Quota fieldsWhen a response includes lookups_remaining, it also includes unlimited. A zero balance is not exhausted when unlimited is true.

Rate limits

TierLimit
Sandbox30 requests/min
Paid60 requests/min
Over limit429 + Retry-After header (seconds)

Pagination (carriers)

FieldDetail
limitdefault 100, max 500
cursoropaque continuation from pagination.next_cursor
Stoppagination.next_cursor is null
Sortestimated_expiration_date asc, then DOT asc

Filters (carriers)

ParamTypeDefaultRange / notes
power_units_mininteger11Min census power_units
renewal_days_mininteger85Inclusive days from current UTC; 0–365
renewal_days_maxinteger95Inclusive; 0–365
renewal_date_minISO date—With renewal_date_max, instead of renewal_days_*; UTC today to today+365
renewal_date_maxISO date—Inclusive; on or after renewal_date_min
statecodes—Comma-separated, e.g. TX,OH; max 60; unknown codes are a 400
exclude_statecodes—e.g. NY,MA; also drops carriers with no state; not with state
lower48booleanfalse48 contiguous states + DC; combine with exclude_state, not with state
operating_statusactive | anyanyany (default) keeps every carrier; active = census status_code A
authoritycommon | contract | broker | any_active | none | anyanyFMCSA authority status Active; broker counts as an authority; none includes carriers with no authority record
has_mcboolean—true = has an MC number, false = none; omit for both

Caps (exports)

CapDetail
Max rows per job10,000 — paginated from the carrier list warehouse view
Max concurrent jobs3 non-terminal (queued or building) exports per owner at a time — 409 past that
Artifact TTL24 hours from creation, then the job expires and the CSV is deleted
Signed downloadUse the absolute signed URL returned by status within 5 minutes; poll status again for a fresh URL
email_when_readyBest-effort only. Sent when Resend is configured; the export still reports ready if the email fails

Errors

400

GET /v2/profile[-lite] needs exactly one of dot, mc, name, vin, phone, or email; export filters must be integers in range; include accepts only contacts (code invalid_include).

401

Missing or invalid Bearer.

403

A paid monthly API key is required for carrier-list, autocomplete, monitoring, and exports requests.

404

No warehouse hit for dot/mc/name/vin/phone/email, the DOT is not on your monitoring list, or the export id is not yours.

409

Monitoring list is at capacity, or mc/name/vin/phone/email on GET /v2/profile[-lite] matched more than one carrier — see candidates[], or you already have 3 exports in progress.

429

Too many requests. Sandbox 30/min. Paid 60/min. Retry-After is seconds.

Retry-After: <seconds>

Header value is seconds until retry.

503

Carrier list or monitoring temporarily unavailable. Retry later. Code contacts_unavailable: contact suppression could not be checked, or an export was invalidated by a newer suppression (no lookup consumed).

Sandbox fixtures

Sandbox keys resolve these fixture DOTs on GET /v2/profile and GET /v2/profile-lite — same response shapes as paid, 30 requests/min.

DOTNote
54283Mega carrier — Swift Transportation. Soft-bound partial dossier: empty contact_history, no_data reliability signals, risk_index.state unavailable (see the Profile example above).
86876Standard fixture carrier
80806Standard fixture carrier
21800Standard fixture carrier
3706Standard fixture carrier
63585Standard fixture carrier
264184Standard fixture carrier
511412Standard fixture carrier
53467Standard fixture carrier
354406Standard fixture carrier

Full sandbox allowlist is 50 DOTs (MDS_SANDBOX_DOTS in source) — the first 10 are listed here.

Contact data

Contact Data is phone, cell_phone, email, contact_name and officer names. Paid keys receive it by default. include=contacts remains accepted as a compatibility no-op.

RouteContact data returned by default
GET /v2/profilecarrier.contacts, identity.officers, and the phone, cell_phone, email and officer rows of contact_history
GET /v2/carrierscarriers[].contacts
POST /v2/exportsphone, cell_phone, email and contact_name CSV columns (query parameter or body field include)
FieldTypeNotes
contacts.phonestring | nullPrimary business phone on file
contacts.cell_phonestring | nullReturned independently even when it equals phone
contacts.emailstring | nullPrimary email on file
contacts.contact_namestring | nullPrimary contact or officer name on file

Opted-out values (contact opt-out) come back as null. When a carrier's Contact Data is suppressed as a whole, contacts are null, identity.officers and contact_history are empty, and physical/mailing street addresses are blanked. A failed suppression check returns 503 before charging the lookup.

Profile

GET/v2/profile

Fat warehouse dossier for one carrier — identity through as-of in a single GET.

ISS-CSA from warehouse SMS. Official flags from Otto. Inspection-unit VINs from vPIC. Contact history from Motus daily-diff, forward-only. Reliability signals from Otto. Not a 0–100 score. Risk index from Otto dossier facts with contributing factors. Not an opaque probability. Provide exactly one of dot, mc, name, vin, phone, or email — a mc/name/vin/phone/email match with more than one carrier returns 409 with a compact candidates[] list ({ dot, mc, name }) instead of guessing. Phone accepts 10 digits with common punctuation and an optional leading +1. Email is trimmed, lowercased, and matched exactly. Registration reverse lookup is deferred until an indexed warehouse column exists. Autocomplete does not search VIN, phone, or email.

Parameters

NameInTypeRequiredDescription
dotquerystringone of dot, mc, name, vin, phone, emailUSDOT; examples use 54283
mcquerystringone of dot, mc, name, vin, phone, emailMC/MX docket, e.g. MC136818
namequerystringone of dot, mc, name, vin, phone, emailLegal or DBA name contains; 2+ characters. 409 if it matches more than one carrier
vinquerystringone of dot, mc, name, vin, phone, emailStored MCMIS inspection-unit VIN; exactly 17 chars after trim/uppercase. 404 if unknown. 409 if ambiguous
phonequerystringone of dot, mc, name, vin, phone, emailExact stored business or cell phone; 10 digits after common punctuation and optional +1 normalization. 404 if unknown. 409 if ambiguous
emailquerystringone of dot, mc, name, vin, phone, emailExact stored business email; trimmed and lowercased, up to 254 characters. 404 if unknown. 409 if ambiguous
includequerystringnocontacts — accepted as a compatibility no-op. See Contact data

Request

curl "https://mds.motorcarrier.ai/v2/profile?dot=54283" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile?mc=MC136818" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile?name=Swift%20Transportation" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile?vin=1M8GDM9AXKP042788" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile?phone=%2B1%20%28555%29%20123-4567" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile?email=dispatch%40carrier.example" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
identityobjectCore carrier identity
identity.dot_numberstring
identity.mc_numberstring
identity.legal_namestring
identity.dba_namestring | null
identity.status_codestringe.g. A
identity.allowed_to_operateboolean
identity.physicalobjectstreet, city, state, zip, country
identity.power_unitsnumber
identity.total_driversnumber
identity.mcs150_datestringdate
authorityobject
authority.current.docket_numberstring
issobjectWarehouse SMS ISS-CSA
iss.valuenumber
iss.recommendationstring
iss.algorithmstringe.g. ISS-CSA
flagsarrayOfficial flags; items { id, on, state }
inspection_unitsarrayItems may include vin, year, make, model
identity.officersstring[]Contact Data returned by default for paid keys unless contact suppression applies
contactsobjectphone, cell_phone, email, contact_name. See Contact data
contact_historyarrayBackward-compatible legal, address, phone, cell_phone, email, and officer history
insurancearrayFilings on file from all sources: docket_number, insurance_type, form_code, company_name, policy_number, effective_date, cancel_effective_date, source
insurance[].estimated_expiration_datestring | nullESTIMATE, never verified: FMCSA filings stay in force until cancelled and no source reports a policy term. Primary BIPD filings only; null for cargo, bond, excess and cancelled filings
insurance[].expiration_basisstring | nullcancellation_filed | replacement_filed (cancellation on file) · future_policy_effective (later filing on file) · anniversary | anniversary_rolled (effective-date anniversary)
insurance[].expiration_verifiedbooleanAlways false
insurance[].renewal_confidencestring | nullevent (a filing on record) | modeled (anniversary assumption)
reliability_signalsobjectstate, as_of, signals[] { id, state }
risk_indexobjectstate, score, baseline, excluded_count, as_of, factors[] — not an opaque probability
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "identity": {
    "dot_number": "54283",
    "mc_number": "136818",
    "legal_name": "SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC",
    "dba_name": null,
    "status_code": "A",
    "allowed_to_operate": true,
    "physical": {
      "street": "2200 S 75TH AVE",
      "city": "PHOENIX",
      "state": "AZ",
      "zip": "85043",
      "country": "US"
    },
    "power_units": 12940,
    "total_drivers": 12883,
    "mcs150_date": "2025-08-05"
  },
  "authority": {
    "current": {
      "docket_number": "136818"
    }
  },
  "iss": {
    "value": 75,
    "recommendation": "Inspect",
    "algorithm": "ISS-CSA"
  },
  "flags": [
    { "id": "shared_phone", "on": false, "state": "clear" },
    { "id": "shared_address", "on": false, "state": "clear" },
    { "id": "revoke_in_36_months", "on": false, "state": "clear" },
    { "id": "authority_gap", "on": false, "state": "clear" },
    { "id": "insurance_not_on_file", "on": false, "state": "clear" },
    { "id": "oos", "on": false, "state": "clear" },
    { "id": "new_entrant", "on": false, "state": "clear" },
    { "id": "name_change_churn", "on": false, "state": "no_data" },
    { "id": "same_vin_multiple_dots", "on": false, "state": "clear" },
    { "id": "shared_email", "on": false, "state": "clear" },
    { "id": "shared_officer", "on": false, "state": "clear" },
    { "id": "prior_revoke", "on": false, "state": "clear" },
    { "id": "not_allowed_to_operate", "on": false, "state": "clear" },
    { "id": "hazmat", "on": false, "state": "clear" },
    { "id": "authority_pending", "on": false, "state": "clear" },
    { "id": "sms_basic_alert", "on": false, "state": "no_data" },
    { "id": "crash_in_12_months", "on": false, "state": "clear" },
    { "id": "inspection_oos", "on": false, "state": "clear" }
  ],
  "inspection_units": [
    { "vin": "1M8GDM9AXKP042788", "year": "1998", "make": "MCI", "model": null }
  ],
  "contact_history": [],
  "reliability_signals": {
    "state": "empty",
    "as_of": null,
    "signals": [
      { "id": "boc3_on_file", "state": "no_data" },
      { "id": "unbroken_coverage_12mo", "state": "no_data" },
      { "id": "authority_60mo", "state": "no_data" },
      { "id": "never_revoked", "state": "no_data" },
      { "id": "recent_mcs150", "state": "no_data" }
    ]
  },
  "risk_index": {
    "state": "unavailable",
    "score": null,
    "baseline": 50,
    "excluded_count": 24,
    "as_of": null,
    "factors": [
      { "id": "flag_oos", "label": "Out of service", "direction": "excluded", "weight": 14, "points": null, "source": "flags" }
    ]
  },
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Calling profile in a tight typeahead loop — use autocomplete + profile-lite first
  • Treating risk_index.score / iss.value as a purchasable “grade” — document as warehouse fields only
  • Ignoring empty arrays / no_data states on mega carriers
  • Treating insurance[].estimated_expiration_date as a verified policy expiration — it is an estimate; expiration_verified is always false
  • Sending more than one of dot/mc/name/vin/phone/email (400) or expecting a silently-picked winner on ambiguous name/mc/vin/phone/email matches (409)

Profile lite

GET/v2/profile-lite

Lean official identity, address, and fleet after typeahead — no full dossier, no scores added.

Provide exactly one of dot, mc, name, vin, phone, or email — a mc/name/vin/phone/email match with more than one carrier returns 409 with a compact candidates[] list ({ dot, mc, name }) instead of guessing. Phone accepts 10 digits with common punctuation and an optional leading +1. Email is trimmed, lowercased, and matched exactly. Registration reverse lookup is deferred until an indexed warehouse column exists.

Parameters

NameInTypeRequiredDescription
dotquerystringone of dot, mc, name, vin, phone, emailUSDOT
mcquerystringone of dot, mc, name, vin, phone, emailMC/MX docket, e.g. MC136818
namequerystringone of dot, mc, name, vin, phone, emailLegal or DBA name contains; 2+ characters. 409 if it matches more than one carrier
vinquerystringone of dot, mc, name, vin, phone, emailStored MCMIS inspection-unit VIN; exactly 17 chars after trim/uppercase. 404 if unknown. 409 if ambiguous
phonequerystringone of dot, mc, name, vin, phone, emailExact stored business or cell phone; 10 digits after common punctuation and optional +1 normalization. 404 if unknown. 409 if ambiguous
emailquerystringone of dot, mc, name, vin, phone, emailExact stored business email; trimmed and lowercased, up to 254 characters. 404 if unknown. 409 if ambiguous

Request

curl "https://mds.motorcarrier.ai/v2/profile-lite?dot=54283" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile-lite?mc=MC136818" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile-lite?name=Swift%20Transportation" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile-lite?vin=1M8GDM9AXKP042788" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile-lite?phone=5551234567" \
  -H "Authorization: Bearer YOUR_API_KEY"

curl "https://mds.motorcarrier.ai/v2/profile-lite?email=dispatch%40carrier.example" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
carrierobjectLean carrier
carrier.dot_numberstring
carrier.mc_numberstring
carrier.legal_namestring
carrier.dba_namestring | null
carrier.status_codestring
carrier.allowed_to_operateboolean
carrier.physicalobjectstreet/city/state/zip/country
carrier.mailingobjectstreet/city/state/zip/country
carrier.power_unitsnumber
carrier.total_driversnumber
carrier.mcs150_datestring
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleantrue for unlimited paid keys; false for metered and sandbox keys

Example JSON

{
  "carrier": {
    "dot_number": "54283",
    "mc_number": "136818",
    "legal_name": "SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC",
    "dba_name": null,
    "status_code": "A",
    "allowed_to_operate": true,
    "physical": {
      "street": "2200 S 75TH AVE",
      "city": "PHOENIX",
      "state": "AZ",
      "zip": "85043",
      "country": "US"
    },
    "mailing": {
      "street": "PO BOX 29243",
      "city": "PHOENIX",
      "state": "AZ",
      "zip": "85038",
      "country": "US"
    },
    "power_units": 12940,
    "total_drivers": 12883,
    "mcs150_date": "2025-08-05"
  },
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Expecting flags / ISS / inspections here — use profile
  • Omitting dot/mc/name/vin/phone/email, or sending more than one of them (400)
  • Expecting a silently-picked winner on ambiguous name/mc/vin/phone/email matches (409)

Carrier list

GET/v2/carriers

Paid lean hits for daily lead ingest — every carrier with a current BIPD filing from any insurance source, filtered by power units, estimated expiration window, state, operating status, authority and MC number.

Defaults target estimated expirations 85–95 days from current UTC. Stable order: estimated_expiration_date asc, DOT asc. Continue with next_cursor as cursor; it preserves the original filters while warehouse rows refresh. Estimates come from a nightly all-source index (meta.as_of_date); until its first refresh completes, the list is served from the earlier Motus observation set (meta.source), and a cursor from that set returns 400 cursor_expired once the index is live — restart without cursor. The authority filter needs that index: before its first refresh it returns 503 filter_unavailable. Invalid or conflicting filter values return 400.

Parameters

NameInTypeRequiredDefaultDescription
power_units_minqueryintegerno11Min power_units
renewal_days_minqueryintegerno85Inclusive; 0–365
renewal_days_maxqueryintegerno95Inclusive; 0–365
renewal_date_minqueryISO dateno—Absolute window start (UTC today to today+365); send with renewal_date_max instead of renewal_days_*
renewal_date_maxqueryISO dateno—Absolute window end, inclusive
statequerystringno—Include these physical states, e.g. TX,OH (max 60)
exclude_statequerystringno—Exclude these states, e.g. NY,MA; carriers with no state are excluded too
lower48querybooleannofalse48 contiguous states + DC; with exclude_state, minus those
operating_statusquerystringnoanyany (default) keeps every carrier; active = census status_code A
authorityquerystringnoanycommon | contract | broker | any_active | none (Active FMCSA authority; broker counts as an authority; none includes carriers with no authority record)
has_mcquerybooleanno—true = has an MC number, false = none
cursorquerystringno—Opaque next_cursor from the prior response; it carries every filter, so send no filters with it (blank values count as omitted)
limitqueryintegerno100Max 500
includequerystringno—contacts — accepted as a compatibility no-op

Request

curl "https://mds.motorcarrier.ai/v2/carriers?power_units_min=11&renewal_days_min=85&renewal_days_max=95&limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
carriersarrayLean hits
carriers[].dotstring
carriers[].namestring
carriers[].power_unitsnumber
carriers[].inferred_renewal_datestringDeprecated alias of estimated_expiration_date
carriers[].days_to_renewalnumberestimated_expiration_date minus today (UTC), computed per request
carriers[].statestring
carriers[].mc_numberstring | null
carriers[].status_codestring | nullCensus operating status
carriers[].estimated_expiration_datestringOtto estimate — not a guarantee, never an FMCSA-filed expiration
carriers[].expiration_basisstringcancellation_filed | replacement_filed | future_policy_effective | anniversary | anniversary_rolled
carriers[].renewal_confidencestringevent (filed cancellation or future filing) | modeled (anniversary)
carriers[].expiration_verifiedbooleanAlways false — no source reports a policy term
carriers[].bipd_sourcesstring[]Insurance sources reporting the current BIPD filings
carriers[].insurerstring | null
carriers[].latest_effective_datestring | nullLatest in-force BIPD effective date
carriers[].contactsobjectphone, cell_phone, email, contact_name; returned by default
pagination.limitnumber
pagination.totalnumberFixed from the first page
pagination.next_cursorstring | nullPass as cursor; null is the stop condition
meta.sourcestringcarrier_renewal_index | insurance_renewal_observations (fallback before the first index refresh)
meta.as_of_datestring | nullIndex refresh date; null on the fallback

Example JSON

{
  "carriers": [
    {
      "dot": "218563",
      "name": "EXAMPLE FREIGHT LLC",
      "power_units": 25,
      "inferred_renewal_date": "2026-12-15",
      "days_to_renewal": 90,
      "state": "TX",
      "mc_number": "MC123456",
      "status_code": "A",
      "estimated_expiration_date": "2026-12-15",
      "expiration_basis": "anniversary",
      "renewal_confidence": "modeled",
      "expiration_verified": false,
      "bipd_sources": ["dot_data_portal", "motus_insur_all_c5y8-a4uz"],
      "insurer": "EXAMPLE MUTUAL INSURANCE CO",
      "latest_effective_date": "2025-12-15"
    }
  ],
  "pagination": {
    "limit": 100,
    "total": 1,
    "next_cursor": null
  },
  "meta": {
    "source": "carrier_renewal_index",
    "as_of_date": "2026-09-16"
  }
}

Common mistakes

  • Using a sandbox key (403)
  • Not continuing until next_cursor is null
  • Treating estimated_expiration_date as FMCSA-filed truth — check expiration_basis
  • Reusing a cursor after a 400 cursor_expired instead of restarting without cursor
  • Combining state with exclude_state or lower48, or renewal_date_* with renewal_days_* (400)

Autocomplete

GET/v2/autocomplete

Paid typeahead over DOT, MC, legal name, or DBA — lean hits, no risk claim.

Default limit: 10 hits. Request profile separately for dossier.

Parameters

NameInTypeRequiredDescription
qquerystringyesMinimum 2 characters after trimming. Accepts DOT, MC, legal name, or DBA name.

Request

curl "https://mds.motorcarrier.ai/v2/autocomplete?q=Swift%20Transportation" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
querystringEcho
resultsarrayLean hits
results[].dot_numberstring
results[].mc_numberstring
results[].legal_namestring
results[].dba_namestring | null
results[].status_codestring
results[].allowed_to_operateboolean
results[].physical_citystring
results[].physical_statestring
results[].power_unitsnumber
results[].total_driversnumber
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "query": "Swift Transportation",
  "results": [
    {
      "dot_number": "54283",
      "mc_number": "136818",
      "legal_name": "SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC",
      "dba_name": null,
      "status_code": "A",
      "allowed_to_operate": true,
      "physical_city": "PHOENIX",
      "physical_state": "AZ",
      "power_units": 12940,
      "total_drivers": 12883
    }
  ],
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • q length < 2
  • Sandbox key (403)
  • Expecting full dossier in results

Monitoring: list

GET/v2/monitoring

Lists DOTs your paid key monitors — identity from the stored watch snapshot, not a fresh dossier pull.

Parameters

None — lists every watch owned by the key's account.

Request

curl "https://mds.motorcarrier.ai/v2/monitoring" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
itemsarray
items[].dot_numberstring
items[].watched_atstringISO timestamp
items[].legal_namestringFrom snapshot
items[].dba_namestring | null
items[].status_codestring
items[].allowed_to_operateboolean
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "items": [
    {
      "dot_number": "54283",
      "watched_at": "2026-09-01T12:00:00.000Z",
      "legal_name": "SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC",
      "dba_name": null,
      "status_code": "A",
      "allowed_to_operate": true
    }
  ],
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Expecting live dossier freshness on list — call profile for fresh
  • Sandbox key (403)

Monitoring: add

POST/v2/monitoring

Adds a DOT to the paid monitoring list — seeds a full snapshot when available, with a partial census snapshot for very large carriers.

201 newly watched; 200 already on list; 404 not in warehouse; 409 at capacity.

Parameters

NameInTypeRequiredDescription
dot_numberbody JSONstringyes*USDOT
dotbody JSONstringyes*Accepted alias

Request

curl -X POST "https://mds.motorcarrier.ai/v2/monitoring" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dot_number": "54283"}'

Response fields

FieldTypeNotes
itemobjectSame lean watch shape as list items
createdbooleantrue if newly added
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "item": {
    "dot_number": "54283",
    "watched_at": "2026-09-01T12:00:00.000Z",
    "legal_name": "SWIFT TRANSPORTATION COMPANY OF ARIZONA LLC",
    "dba_name": null,
    "status_code": "A",
    "allowed_to_operate": true
  },
  "created": true,
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Empty body / missing both aliases
  • Watching a DOT not in warehouse (404)
  • Ignoring 409 capacity

Monitoring: remove

DELETE/v2/monitoring/{dot}

Removes a DOT from the paid monitoring list.

404 if not currently on the list.

Parameters

NameInTypeRequiredDescription
dotpathstringyesUSDOT

Request

curl -X DELETE "https://mds.motorcarrier.ai/v2/monitoring/54283" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
removedboolean
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "removed": true,
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Removing a DOT that isn’t currently on the list (404)

Monitoring: events

GET/v2/monitoring/events

Cursor-paged change feed for watched DOTs — honest warehouse fields only, areas limited to authority, insurance, and safety.

Events are produced by a poll cycle that diffs each watch's current warehouse snapshot against its stored baseline. No invented scores; census/contact/status-only diffs are not emitted here.

Parameters

NameInTypeRequiredDefaultDescription
cursorquerystringnononeOpaque cursor from a prior response's next_cursor
limitqueryintegerno100Max 500

Request

curl "https://mds.motorcarrier.ai/v2/monitoring/events?limit=100"   -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
eventsarrayMay be empty
events[].idstringuuid
events[].dot_numberstring
events[].occurred_atstringISO timestamp
events[].areastringauthority | insurance | safety
events[].fieldstring
events[].kindstringchanged | added | removed
events[].beforestring | null
events[].afterstring | null
next_cursorstring | nullPass back as cursor to continue; null means caught up
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "events": [
    {
      "id": "8f6c9e2a-2f0a-4e2b-9a7a-1d9b6a7f3c11",
      "dot_number": "54283",
      "occurred_at": "2026-09-17T12:00:00.000Z",
      "area": "authority",
      "field": "common_status",
      "kind": "changed",
      "before": "A",
      "after": "I"
    }
  ],
  "next_cursor": "MjAyNi0wOS0xN1QxMjowMDowMC4wMDBafDhmNmM5ZTJhLTJmMGEtNGUyYi05YTdhLTFkOWI2YTdmM2MxMQ",
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Dropping next_cursor between polls — always resume from the last cursor
  • Sandbox key (403)
  • Expecting census/contact-only changes here — only authority/insurance/safety areas are emitted

Webhooks: register

POST/v2/monitoring/webhooks

Registers an https:// endpoint to receive signed POSTs for new monitoring events instead of polling.

The secret is returned once at creation — store it, then verify X-MDS-Signature as sha256= + hex HMAC-SHA256 of X-MDS-Timestamp + . + the raw body using that secret, and reject timestamps older than 5 minutes.

Parameters

NameInTypeRequiredDescription
urlbody JSONstringyeshttps:// endpoint

Request

curl -X POST "https://mds.motorcarrier.ai/v2/monitoring/webhooks"   -H "Authorization: Bearer YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{"url": "https://example.com/mds-webhook"}'

Response fields

FieldTypeNotes
idstringuuid
urlstring
secretstringShown once — save it now
created_atstringISO timestamp

Example JSON

{
  "id": "3b1c9e2a-2f0a-4e2b-9a7a-1d9b6a7f3c22",
  "url": "https://example.com/mds-webhook",
  "secret": "mds_whsec_5f0c1a2b3d4e5f60718293a4b5c6d7e8",
  "created_at": "2026-09-17T12:00:00.000Z"
}

Common mistakes

  • Non-https:// url (400)
  • Losing the one-time secret — revoke and re-register instead of guessing it

Webhooks: list

GET/v2/monitoring/webhooks

Lists active webhook registrations for the key's account — no secrets returned.

Parameters

None — lists every active webhook owned by the key's account.

Request

curl "https://mds.motorcarrier.ai/v2/monitoring/webhooks"   -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
webhooksarrayMay be empty
webhooks[].idstringuuid
webhooks[].urlstring
webhooks[].created_atstringISO timestamp

Example JSON

{
  "webhooks": [
    {
      "id": "3b1c9e2a-2f0a-4e2b-9a7a-1d9b6a7f3c22",
      "url": "https://example.com/mds-webhook",
      "created_at": "2026-09-17T12:00:00.000Z"
    }
  ]
}

Common mistakes

  • Expecting secret here — it is never returned after creation
  • Sandbox key (403)

Webhooks: revoke

DELETE/v2/monitoring/webhooks/{id}

Revokes a webhook — delivery stops immediately.

404 if the id isn't an active webhook owned by this key's account.

Parameters

NameInTypeRequiredDescription
idpathstringyesWebhook id from register/list

Request

curl -X DELETE "https://mds.motorcarrier.ai/v2/monitoring/webhooks/3b1c9e2a-2f0a-4e2b-9a7a-1d9b6a7f3c22"   -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
revokedboolean

Example JSON

{
  "revoked": true
}

Common mistakes

  • Revoking an id that isn’t currently active (404)

Export: enqueue

POST/v2/exports

Queues an async CSV export over the same filter spirit as carrier list — up to 10,000 rows, built by a background worker. Same all-source universe, filters and order as carrier list. Contact columns (phone, cell_phone, email, contact_name) are included by default for paid keys.

Max 3 non-terminal (queued/building) exports per owner at once — a 4th returns 409. Consumes one paid lookup on enqueue, not on status polling. authority (other than any) returns 503 filter_unavailable, without consuming a lookup, until the first all-source index refresh; the other filters work before it. Body booleans accept JSON true/false or the strings true/false, integers accept numbers or digit strings, and an empty string counts as omitted.

Parameters

NameInTypeRequiredDefaultDescription
power_units_minbody JSONintegerno11Min power_units
renewal_days_minbody JSONintegerno85Inclusive; 0–365
renewal_days_maxbody JSONintegerno95Inclusive; 0–365
renewal_date_min, renewal_date_max, state, exclude_state, lower48, operating_status, authority, has_mcbody JSONas carrier listno—Same values and 400 rules as GET /v2/carriers; state and exclude_state may also be arrays
email_when_readybody JSONbooleannofalseBest-effort email when the CSV is ready
includequery or body JSONstring | string[]no—contacts — accepted as a compatibility no-op

Request

curl -X POST "https://mds.motorcarrier.ai/v2/exports"   -H "Authorization: Bearer YOUR_API_KEY"   -H "Content-Type: application/json"   -d '{"power_units_min": 11, "renewal_days_min": 85, "renewal_days_max": 95, "email_when_ready": true}'

Response fields

FieldTypeNotes
idstringExport job id — poll with GET /v2/exports/{'{id}'}
statestringqueued | building | ready | failed | expired
include_contactsbooleantrue when the CSV has the contact columns
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "id": "6a9f2b5e-1e2c-4a3b-9c1d-8f7e6d5c4b3a",
  "state": "queued",
  "include_contacts": true,
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Polling faster than the worker cadence — it claims jobs every 1–2 minutes
  • Starting a 4th concurrent export before an earlier one finishes (409)
  • Assuming email_when_ready guarantees delivery — it is best-effort and fails open

Export: status

GET/v2/exports/{id}

Polls an owned export job — returns an absolute short-lived signed CSV URL once state is ready.

404 if the id does not exist or is not owned by this key's account. Status rechecks current per-DOT suppression before signing and purges a stale contact export. Poll status again for a fresh URL after 5 minutes.

Parameters

NameInTypeRequiredDescription
idpathstringyesExport job id from the enqueue response

Request

curl "https://mds.motorcarrier.ai/v2/exports/6a9f2b5e-1e2c-4a3b-9c1d-8f7e6d5c4b3a"   -H "Authorization: Bearer YOUR_API_KEY"

Response fields

FieldTypeNotes
idstring
statestringqueued | building | ready | failed | expired
urlstring | nullAbsolute signed CSV URL; present only when ready and valid for 5 minutes
errorstring | nullFailure detail when state is failed
row_countnumber | nullRows written; present once building finishes
include_contactsbooleantrue when the CSV has the contact columns
lookups_remainingnumberMay be 0 for unlimited keys
unlimitedbooleanInterpret the numeric balance with this flag

Example JSON

{
  "id": "6a9f2b5e-1e2c-4a3b-9c1d-8f7e6d5c4b3a",
  "state": "ready",
  "url": "https://storage.example.com/signed/export.csv",
  "error": null,
  "row_count": 4218,
  "include_contacts": true,
  "lookups_remaining": 0,
  "unlimited": true
}

Common mistakes

  • Reusing a signed URL after its 5-minute lifetime — poll status for a fresh URL
  • Treating expired as an error — it just means the 24h artifact TTL passed

Contact opt-out

POST/api/privacy/opt-out

Public, no API key. Starts an opt-out for an email address (required) plus an optional phone and USDOT number. We email a confirmation link; nothing changes until the person confirms it.

People can use the page at mds.motorcarrier.ai/opt-out or write to privacy@motorcarrier.ai. Once confirmed, the email comes back as null on /v2/profile, /v2/carriers, exports and Studio. The phone is suppressed (for that carrier only) when it is on file with the verified email for that USDOT number; otherwise our team reviews it within 10 business days. The 202 body is the same whether or not we hold the address. If your own customers ask you to stop using their details, forward the request to privacy@motorcarrier.ai.

Parameters

NameInTypeRequiredDescription
emailbodystringyesTrimmed and lowercased
phonebodystringno10-digit US number or E.164
dot_numberbodystringnoThe phone is suppressed for this carrier when it and the verified email are both on file for it; otherwise it is reviewed within 10 business days

Request

curl -X POST "https://mds.motorcarrier.ai/api/privacy/opt-out"   -H "Content-Type: application/json"   -d '{"email":"owner@example.com","phone":"555-201-3344","dot_number":"54283"}'

Response fields

FieldTypeNotes
okbooleantrue on 202
messagestringSame text for every valid request

Example JSON

{
  "ok": true,
  "message": "If that address can receive email, a confirmation link is on its way. Open it within 24 hours to finish the opt-out."
}

Common mistakes

  • Sending an API key — this route is public and ignores it
  • Retrying in a loop — it is limited per IP, per email address (3 a day) and overall (429)
  • Reading the 202 as proof the address exists — it never says either way

Open the contact opt-out page

Recipes

Mega-carrier safe path

  1. Start with profile-lite to confirm identity
  2. Then profile once
  3. Handle empty arrays / state: no_data / risk_index.state: unavailable honestly — do not retry-spam
  4. Soft-fail UI: show what arrived; don't invent missing groups

Change feed

  1. POST /v2/monitoring/webhooks once — save the one-time secret
  2. On delivery, verify X-MDS-Signature as sha256= + hex HMAC-SHA256 of X-MDS-Timestamp + . + the raw body using that secret, and reject timestamps older than 5 minutes
  3. Or skip webhooks and poll GET /v2/monitoring/events, resuming from next_cursor each call
  4. Either path only emits authority/insurance/safety changes — no invented scores

Daily lead pipeline (bulk)

  1. POST /v2/exports with your renewal window filters (up to 10,000 rows/job)
  2. Poll GET /v2/exports/{id} every minute or two until state is ready or failed
  3. Download the CSV from url promptly — the signed link is short-lived and the artifact itself expires after 24h
  4. Optionally set email_when_ready for a best-effort nudge instead of tight polling