SA

REST API - v1

Build with salvage auction data.

One normalized REST API over IAA US/CA/UK and Copart US/CA/UK/DE. JSON in, JSON out, fast cache hits, HMAC-signed webhooks and free credits to start.

Base URL https://salvagealert.com/api/v1OpenAPI 3.0API dashboard

Looking for a dataset? Click here โ†’

3 steps to first call

Quick start

  1. 1Create a free account. 50 credits are granted automatically.
  2. 2Open your API dashboard, copy the sa_live_* key and store it as an environment variable.
  3. 3Run the first request. Your account profile should come back in under 200 ms on a cache hit.
curl https://salvagealert.com/api/v1/me \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "user_id": "u_abc123",
  "email": "you@example.com",
  "enrolled_at": 1780000000,
  "credit_balance": 50,
  "rate_limit_per_minute": 60
}

Runnable examples in cURL, Node and Python, plus a Postman collection, are on GitHub.

Authentication

Every request must send your secret key as a Bearer token in the Authorization header.

Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Create keys from the API dashboard or rotate them with POST /api-keys/rotate. The full secret is shown once; only a SHA-256 hash is stored.
HeaderDescription
X-Request-IdUUID for this request. Quote it when contacting support.
X-Credits-ChargedCredits consumed by this call.
X-Credits-BalanceRemaining credits after this call.
X-RateLimit-LimitPer-minute call ceiling for your key.
X-RateLimit-RemainingCalls you can still make right now. The allowance refills continuously, so this climbs back between calls.
X-RateLimit-ResetUnix seconds at which the allowance is back to full. On a 429, obey Retry-After instead โ€” it names the exact second a slot frees.
X-Credits-WarningSent only when the balance after this call is low or already zero. Top up before the next call 402s.
X-Salvagealert-WarningSomething about the calling key needs attention โ€” today, a key with no project URL, which will be disabled. Log it; its presence is the signal.

Errors

Errors always use the same JSON shape and a stable error.code that you can handle programmatically.

{
  "error": {
    "code": "insufficient_credits",
    "message": "This call costs 1 credit; your balance is 0."
  },
  "request_id": "5f3d6e92-1234-5678-9abc-def012345678"
}
StatuscodeWhen
400invalid_body / invalid_sourceMalformed body or unknown enum value.
400non_https / private_address / private_hostWebhook URL fails the SSRF safety check.
401missing_credentials / invalid_credentialsBad or missing key, inactive account.
402insufficient_creditsTop up before making more paid calls.
403api_disabledAPI access is disabled for this account.
404not_found / lot_not_foundThe resource does not exist or is not owned by this key.
429rate_limitPer-minute ceiling exceeded.
500internal_errorQuote request_id when reporting.

Rate limits

Rate limits are per account and follow your plan: Free 60 requests per minute, Starter 120, Unlimited 300. Every key on the account shares one allowance, so minting more keys does not buy more throughput. The limiter is a token bucket that refills continuously and survives a deploy, so `X-RateLimit-Remaining` is an allowance rather than a window that empties on the hour. Over-limit requests return 429 without charging credits โ€” obey `Retry-After`, which names the second to try again, and read `X-RateLimit-Reset` for when the bucket is full.

Field conventions

A few conventions hold across the whole API. Know these and the responses stop surprising you.

FieldDescription
CasingMost endpoints return snake_case. /lots/search and /lots/{source}/{lot_key} return the raw normalized Lot object, which is camelCase (stockNum, odometerReading, primaryDamage, imageUrl).
Moneycurrent_bid and buy_now are strings (as displayed, may include a currency symbol). The numeric *_value fields โ€” buy_now_value, high_prebid_value, est_retail_value โ€” are plain numbers.
TimestampsFour formats, and which one you get depends on the field, not the endpoint. Epoch seconds is the default โ€” every *_at field, plus last_seen_utc, first_seen_utc and sold_at. Epoch milliseconds is exactly two fields: sale_date_utc and auctionDateUTC (the camelCase Lot), the same instant under two names. /matches.csv writes every timestamp as an ISO-8601 UTC string instead, so a spreadsheet opens it as a date. Usage.per_day[].day and FxRates.date are YYYY-MM-DD calendar days and carry no time zone.
BooleansNot uniform, and both forms sit in the same object. make_offer, wholesale_only, former_rental and offsite are JSON booleans; has_keys, drives and starts are integers (0/1), although all seven come from the same kind of column. In both forms null means the source stated nothing โ€” never false, so a car we know nothing about the keys of is not a car without keys.
CurrencyEach lot's prices are in its source currency (see Sources). /stats/market and other cross-source figures are normalized to USD.

Credits & pricing

Credits are spent only on calls that fetch auction data. Account, reference and webhook management calls are free. Cache hits on search calls cost 0 credits.

EndpointCreditsNotes
POST /lots/search10 on cache hit.
GET /lots/{source}/{lot_key}1Always fresh; bypasses cache.
GET /lots/{source}/{lot_key}/images4IAA + Copart. 50 URLs per prefix (IAA).
GET /lots/{source}/{lot_key}/images.zip8IAA + Copart. ZIP archive.
GET /inventory1Archived index; paginated browse.
GET /inventory/sources0Per-source active-lot counts.
GET /me, /usage, /api-keys/*0Account management.
GET /makes, /models, /yards0Cached catalogue data.
ALL /webhooks/*, /watches/*0Subscription management is free.
GET /vehicles/*, /fx, /fees/estimate0.2VIN, safety, fuel economy, FX and fee lookups.
GET /vin/{vin}/history1Relisting trail across every source.
GET /stats/market1Aggregate price stats, USD-normalized.
GET /matches.csv0Export your own match history.
GET /vin/{vin}/title-check1Salvage / branded-title verdict for a VIN.
POST /vin/batch1 / VINBulk VIN history, max 50 VINs per call.
Every signup includes 50 free credits. Need more volume? Buy API credit top-up packs (1k / 5k / 20k) directly from your dashboard at /app/api โ€” they apply instantly, no support ticket needed.

Sources & currencies

Use one source identifier per call. The Lot schema stays the same; currency and location format vary by source.

sourceRegionCurrencyLocation format
iaai_usIAA - USAUSDState codes, e.g. US-FL
iaai_caIAA - CanadaCADProvincial branches
iaai_ukIAA - UK (SYNETIQ)GBPUK-wide (no regional facet)
copart_usCopart - USAUSDState codes, e.g. US-TX
copart_caCopart - CanadaCADProvince codes, e.g. CA-QC
copart_ukCopart - UKGBPNation codes
copart_deCopart - GermanyEURBundesland codes

No credits required

Free to test

You can wire up and verify your whole integration without spending a single credit. The endpoints below always cost 0, so confirm your key, headers and JSON parsing before touching paid auction data. Every signup also includes 50 free credits to spend when you are ready.

Always free โ€” 0 credits

EndpointNotes
GET /meConfirms your key works and returns your credit balance.
GET /usagePer-day call and credit history.
GET /makesFull make catalogue.
GET /models?make_id=230Models for one make (Honda = 230).
GET /yards?source=copart_usYard list for a source.
POST /lots/searchFree on any cache hit โ€” see the note below.

Verify your key โ€” 0 credits

# 0 credits โ€” confirm your key, then list the make catalogue
curl https://salvagealert.com/api/v1/me \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"

curl https://salvagealert.com/api/v1/makes \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"

Known-good test fixtures

When you are ready to spend a credit, use these verified values so your first paid call returns real data instead of an empty result.

FixtureValueNotes
VIN decode1HGCM82633A0043522003 Honda Accord โ€” decodes fully.
Copart US lot50567636GET /lots/copart_us/50567636.
IAA US lot50567636Has an enumerable image gallery.
SafetyHonda / Accord / 2018Recalls + NHTSA ratings.
Fuel economyToyota / Camry / 2020EPA MPG, fuel type and CO2.
FX ratesbase=USD&symbols=EUR,CAD,GBPDaily reference rates.
Every search response includes a cached boolean and an X-Credits-Charged header. When cached is true the call cost you nothing โ€” popular queries are shared across all users, so a repeat of the example query above is usually free.

Lots

POST/v1/lots/search1 cr (0 on cache hit)

Search lots from one source with filter pushdown. Returns normalized Lot objects per page.

Body parameters

FieldTypeReqDescription
sourcestringrequirediaai_us / iaai_ca / iaai_uk / copart_us / copart_ca / copart_uk / copart_de / govdeals_us
keywordstringโ€”Free text matched against make, model and brand.
makesstring[]โ€”Example: HONDA, TOYOTA.
modelsstring[]โ€”Example: CIVIC.
year_min, year_maxintegerโ€”Inclusive year range.
locationsstring[]โ€”State, province, nation or emirate codes.
title_typesstring[]โ€”clean, salvage, rebuilt, parts.
buy_now_onlybooleanโ€”Restrict to Buy Now lots.
min_buy_now / max_buy_nownumberโ€”Buy Now range in source currency.
min_high_bid / max_high_bidnumberโ€”Current bid range.
min_odometer_km / max_odometer_kmnumberโ€”Odometer range in km.
damage_keywordsstring[]โ€”Substring match on primary damage.
pageintegerโ€”1-based. Default 1.
page_sizeintegerโ€”1 to 100. Default 25.

Example

curl https://salvagealert.com/api/v1/lots/search \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "copart_de",
    "makes": ["MERCEDES-BENZ"],
    "year_min": 2020,
    "title_types": ["salvage"],
    "page_size": 10
  }'
ResponseJSON
{
  "source": "copart_de",
  "page": 1,
  "page_size": 10,
  "total_estimate": 1042,
  "cached": false,
  "lots": [{ "stockNum": "EU52836712", "year": 2021, "make": "MERCEDES-BENZ" }]
}
GET/v1/lots/{source}/{lot_key}1 cr (always fresh)

Single-lot detail. Bypasses cache so bid and sale events are live.

curl https://salvagealert.com/api/v1/lots/copart_us/50567636 \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
GET/v1/lots/{source}/{lot_key}/images4 cr (IAA + Copart)

Enumerate every full-resolution gallery URL for the lot โ€” far more than the listing-page thumbnail. IAA lots enumerate via the resizer (optional width/height/limit params); Copart returns its full high-res set.

curl https://salvagealert.com/api/v1/lots/iaai_us/41678234/images \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
// IAA โ€” enumerated via the resizer (width/height/limit apply)
{
  "source": "iaai_us",
  "lot_key": "41678234",
  "count": 42,
  "width": 1600,
  "height": 1200,
  "images": [
    "https://vis.iaai.com/resizer?imageKeys=41678234~SID~B12~S0~I1&width=1600&height=1200"
  ]
}

// Copart โ€” full high-res set straight from the public gallery
{
  "source": "copart_us",
  "lot_key": "50567636",
  "count": 38,
  "images": [
    "https://cs.copart.com/v1/AUTH_svc.pdoc00001/PIX232/.../hrs.JPG"
  ]
}
GET/v1/lots/{source}/{lot_key}/images.zip8 cr (IAA + Copart)

Stream a server-built ZIP archive of every photo for the lot. IAA archives also include the exterior-360 frames; Copart includes its full high-res gallery. application/zip with Content-Disposition attachment; X-Photos-Included reports how many made it in.

curl -O -J https://salvagealert.com/api/v1/lots/iaai_us/41678234/images.zip \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
GET/v1/vin/{vin}/history1 cr

Every distinct lot we've archived for a full 17-character VIN, across every source โ€” the relisting and flip trail. Copart lots carry full, unmasked VINs across the United States, Canada, the United Kingdom and Germany, so cross-source matches are reliable. Newest first.

curl https://salvagealert.com/api/v1/vin/1HGCM82633A004352/history \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "vin": "1HGCM82633A004352",
  "count": 2,
  "lots": [{
    "source": "copart_us",
    "source_label": "Copart USA",
    "lot_key": "58821234",
    "vin": "1HGCM82633A004352",
    "year": 2019,
    "make": "HONDA",
    "model": "ACCORD",
    "primary_damage": "FRONT END",
    "title_type": "Repairable",
    "odometer": "84210",
    "sale_date_utc": 1765238400000,
    "current_bid": "4200",
    "est_retail_value": 18750,
    "source_url": "https://www.copart.com/lot/58821234",
    "listing_url": "https://salvagealert.com/lots/copart-us-58821234-2019-honda-accord",
    "last_seen_utc": 1765152000,
    "prior_sales": [
      { "sale_date_utc": 1764460800000, "high_bid_value": 4100, "currency": "USD" }
    ]
  }]
}
GET/v1/vin/{vin}/title-check1 cr

Carfax-lite title check: whether a full 17-character VIN has any salvage-auction record in our IAA/Copart archive and whether any listing carries an explicitly branded title (salvage, rebuilt, flood, junk). Always returns 200 โ€” a missing record is a valid clean-so-far answer, not proof of a clean title.

curl https://salvagealert.com/api/v1/vin/1HGCM82633A004352/title-check \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "vin": "1HGCM82633A004352",
  "found": true,
  "salvage_record": true,
  "branded": true,
  "record_count": 2,
  "sources": ["Copart USA"],
  "title_types": ["Repairable"],
  "damages": ["FRONT END"],
  "first_seen_utc": 1752000000,
  "last_seen_utc": 1765152000,
  "note": "Absence of a record is not proof of a clean title โ€” it means no salvage-auction listing was found in our data."
}
POST/v1/vin/batch1 cr per VIN with results (max 50)

Look up relisting history for many VINs in one call. Charges 1 credit per VIN that returned lots โ€” a VIN we hold nothing for is free; invalid entries are echoed back in invalid and not billed. Each result mirrors the single VIN-history lot shape.

curl -X POST https://salvagealert.com/api/v1/vin/batch \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vins":["1HGCM82633A004352","5YJ3E1EA7KF000316"]}'
ResponseJSON
{
  "requested": 2,
  "looked_up": 2,
  "billed": 1,
  "invalid": [],
  "results": [
    { "vin": "1HGCM82633A004352", "count": 2, "lots": [] },
    { "vin": "5YJ3E1EA7KF000316", "count": 0, "lots": [] }
  ]
}

Inventory

Browse the public archived-inventory index โ€” the same continuously-crawled catalogue behind our /lots pages. It reads pre-archived stock, so it is the cheap, stable way to mirror every crawled source onto your own site.

GET/v1/inventory1 cr

Page through the archived-inventory index. Filter by source, make, model, year and mileage; sort and paginate. Sold lots are opt-in with include_sold=true, each carrying its sold_at.

curl "https://salvagealert.com/api/v1/inventory?source=govdeals_us&page_size=20" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "source": "govdeals_us",
  "page": 1,
  "page_size": 20,
  "total": 3184,
  "sort": "ending",
  "lots": [{
    "source": "govdeals_us",
    "lot_key": "18421-098",
    "year": 2016,
    "make": "FORD",
    "model": "F-250",
    "currency": "USD",
    "current_bid": "$4,250",
    "sale_date_utc": 1781990400000,
    "image_url": "https://...",
    "listing_url": "https://salvagealert.com/lots/2016-ford-f-250-govdeals-us-18421-098",

    "acv": 7101,
    "title_state": "PA",
    "origin_country": "Japan",
    "sale_event_status": "On Approval",
    "sale_light": "RED LIGHT",
    "make_offer": true,
    "wholesale_only": false,
    "former_rental": false,
    "offsite": false,
    "offsite_location": null,
    "lane": "4",
    "run_number": "11",
    "airbags_status": "Intact",
    "loss_type": "Collision",
    "has_keys": 1,
    "drives": 1,
    "starts": 1,
    "trim": "LARIAT"
  }]
}

acv is the insurer's pre-loss valuation and the anchor for judging a bid. sale_event_status tells you whether the high bid can actually win. offsite means the vehicle is not at vehicle_location. null always means the source did not publish the value โ€” not that it is zero or false.

updated_since ยท cursor

Mirror the catalogue and keep it current

Walk the whole index once in keyset mode, then ask only for what changed: updated_since against archived_at, include_sold so a sale reaches you as a row with sold_at, cursor to page through a large delta. Upsert on (source, lot_key). A lot you have not seen for a while is stale, not deleted โ€” the endpoint reports no tombstones.

# First walk: keyset mode, every page until next_cursor is null.
CURSOR=""
while :; do
  PAGE=$(curl -s "https://salvagealert.com/api/v1/inventory?page_size=200&cursor=$CURSOR" \
    -H "Authorization: Bearer $SALVAGEALERT_KEY")
  echo "$PAGE" | jq -c '.lots[]' >> lots.ndjson
  CURSOR=$(echo "$PAGE" | jq -r '.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done
# Later: only what changed since the last run, sold lots included.
SINCE=$(date -d '-1 hour' +%s)
curl -s "https://salvagealert.com/api/v1/inventory?updated_since=$SINCE&include_sold=true&page_size=200" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" | jq '.lots | length'
GET/v1/inventory/{source}/{lot_key}1 cr ยท 0 on a miss

One catalogue lot by source and lot key, off the index, for every source. The read for re-checking a lot you already hold or resolving one a webhook named; the live /lots endpoint is for the freshest bid.

curl https://salvagealert.com/api/v1/inventory/copart_us/58821234 \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "lot": { "source": "copart_us", "lot_key": "58821234", "vin": "1HGCM82633A004352", "year": 2019, "make": "HONDA", "model": "ACCORD",
           "high_prebid_value": 4200, "buy_now_value": null, "sale_date_utc": 1765238400000, "sold_at": null,
           "listing_url": "https://salvagealert.com/lots/2019-honda-accord-copart-us-58821234", "...": "the InventoryLot shape" }
}
GET/v1/inventory/sources0 cr

Live active-lot counts per source in the archived-inventory index. Discover how much stock is available.

curl https://salvagealert.com/api/v1/inventory/sources \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "total": 184302,
  "sources": [
    { "source": "govdeals_us", "label": "GovDeals", "currency": "USD", "active_lots": 3184 },
    { "source": "iaai_us", "label": "IAA USA", "currency": "USD", "active_lots": 98211 }
  ]
}
GET/v1/auctions0

The IAA and Copart sale calendar, one chronological list. A Copart sale carries its yard in the spelling /inventory?yard= takes and how many lots we hold there. Stored rows, refreshed by the worker; nothing is scraped on your call.

ResponseJSON
{
  "sales": [{ "id": "631-09/22/2026", "source": "iaai_us", "title": "Albany", "subtitle": "Schenectady, NY",
              "start_utc": 1790083800000, "vehicles": 338, "url": "https://www.iaai.com/SalesList/631~US/09222026",
              "is_live": false, "yard": null, "catalog_lots": null },
            { "id": "copart-โ€ฆ", "source": "copart", "title": "VA - Hampton", "subtitle": "copart_us",
              "start_utc": 1790091000000, "is_live": false, "yard": "HAMPTON, VA", "catalog_lots": 212 }],
  "copart_stale": false, "copart_checked_at": 1790050000000,
  "iaai_us_upcoming_days": [{ "date_utc": 1790121600000, "vehicles": 4120, "url": "https://www.iaai.com/LiveAuctionsCalendar" }]
}

Account

GET/v1/me0

Profile, current credit balance and rate limit.

ResponseJSON
{"credit_balance":47,"rate_limit_per_minute":60}
GET/v1/usage?days=300

Daily and per-endpoint API usage for the selected window.

ResponseJSON
{"days":30,"per_day":[{"day":"2026-05-01","calls":12,"credits_spent":8}]}
GET ยท POST ยท DELETE/v1/api-keys0

Your API keys, listed, minted one at a time and revoked one at a time โ€” the dashboard's key manager over the API. The secret is returned once.

curl https://salvagealert.com/api/v1/api-keys -H "Authorization: Bearer $SALVAGEALERT_KEY"
curl -X POST https://salvagealert.com/api/v1/api-keys -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" -d '{"label": "staging", "project_url": "https://staging.example.com"}'
curl -X DELETE https://salvagealert.com/api/v1/api-keys/key_9f3cโ€ฆ -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{ "id": "key_9f3cโ€ฆ", "key": "sa_live_โ€ฆ", "prefix": "sa_live_9f3c", "label": "staging", "project_url": "https://staging.example.com", "created_at": 1790053300 }
POST/v1/api-keys/rotate0

Revokes active keys and returns one fresh secret exactly once.

curl -X POST https://salvagealert.com/api/v1/api-keys/rotate \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label":"Production rotated"}'
GET/v1/matches0

Your saved-search matches as JSON, newest first, drawn only from your own searches โ€” the pull path beside the webhook push. Keyset-paged: pass next_cursor back as cursor; null on the last page. Free.

curl "https://salvagealert.com/api/v1/matches?since=1765000000&limit=50" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "matches": [{ "id": 570293, "matched_at": 1790053217, "search_id": 13, "search_name": "trucks", "source": "copart_us",
                "lot_key": "58821234", "year": 2019, "make": "HONDA", "model": "ACCORD", "sale_date_utc": 1790083800000,
                "high_prebid_value": 4100, "buy_now_value": null, "favorited_at": null, "sold_at": null }],
  "next_cursor": "1790053217:570293"
}
GET/v1/matches/{id}0

A single match from your own searches, with your annotations on it.

curl https://salvagealert.com/api/v1/matches/570293 \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
PATCH/v1/matches/{id}0

Write your decisions on a match: favorite, dismiss, the max-bid ceiling, the Buy Now target, a note, the won and resale prices. Send only what changes; null clears a value.

curl -X PATCH https://salvagealert.com/api/v1/matches/570293 \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" -H "Content-Type: application/json" \
  -d '{"favorited": true, "max_bid_value": 4500, "notes": "Check the frame rail in photo 7"}'
ResponseJSON
{
  "match": { "id": 570293, "source": "copart_us", "lot_key": "58821234", "favorited_at": 1790053300,
             "max_bid_value": 4500, "price_alert_value": null, "notes": "Check the frame rail in photo 7",
             "won_at": null, "won_price": null, "resale_price": null, "...": "the Match schema" }
}
GET/v1/matches.csv0

Streams your saved-search match history as a CSV download, drawn only from your own searches. Free โ€” the data was already paid for at poll time. Up to 50,000 rows per call, newest first.

curl -O -J "https://salvagealert.com/api/v1/matches.csv?source=copart_us&limit=5000" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
POST/v1/vin-watches0

Watch one vehicle: a hit on any source sends a lot.matched delivery of kind vin_watch_match, and with a region and sources the watch also runs the VIN as a search every 48 h or on demand. Survives relistings โ€” it keys on the VIN. List, read, edit, pause, delete and run under the same path.

curl -X POST https://salvagealert.com/api/v1/vin-watches \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vin":"1HGCM82633A004352","label":"Client car","region":"us","sources":["copart_us","iaai_us"]}'
ResponseJSON
{
  "id": 42, "vin": "1HGCM82633A004352", "label": "Client car", "active": true, "region": "us",
  "sources": ["copart_us", "iaai_us"], "created_at": 1790053217, "last_match_at": null,
  "last_run_at": null, "can_run_at": null, "matches": [], "events": []
}

List, update, delete

curl https://salvagealert.com/api/v1/vin-watches -H "Authorization: Bearer $SALVAGEALERT_KEY"
curl -X PATCH https://salvagealert.com/api/v1/vin-watches/42 -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" -d '{"active":false}'
curl -X POST https://salvagealert.com/api/v1/vin-watches/42/run -H "Authorization: Bearer $SALVAGEALERT_KEY"
curl -X DELETE https://salvagealert.com/api/v1/vin-watches/42 -H "Authorization: Bearer $SALVAGEALERT_KEY"

Reference data

GET/v1/makes0

Vehicle make catalogue.

ResponseJSON
{"makes":[{"id":160,"name":"FORD","count":120453}]}
GET/v1/models?make_id=2300

Model list for one make.

ResponseJSON
{"make_id":230,"models":[{"id":40510,"name":"CIVIC","count":18230}]}
GET/v1/yards?source=copart_us0

Yard or branch catalogue per source.

ResponseJSON
{"source":"copart_us","yards":[{"code":"DALLAS","name":"Dallas","region":"TX"}]}

Vehicle data

Decode VINs, pull open recalls and crash-test ratings, and look up fuel economy โ€” plus daily reference exchange rates. Each call costs 0.2 credits.

GET/v1/vehicles/vin/{vin}0.2 cr

Decode a 17-character VIN into a structured vehicle spec: year, make, model, trim, body, engine and drivetrain. Pass ?year= to disambiguate older VINs.

curl https://salvagealert.com/api/v1/vehicles/vin/1HGCM82633A004352 \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "vin": "1HGCM82633A004352",
  "year": 2003,
  "make": "HONDA",
  "model": "Accord",
  "trim": "EX",
  "body_class": "Coupe",
  "doors": 2,
  "drive_type": "FWD",
  "engine": { "cylinders": 6, "displacement_l": 3, "horsepower": 240, "fuel_type": "Gasoline" },
  "plant": { "country": "UNITED STATES", "city": "MARYSVILLE", "state": "OHIO" }
}
GET/v1/vehicles/safety0.2 cr

Open recall campaigns plus crash-test safety ratings for a year/make/model.

curl "https://salvagealert.com/api/v1/vehicles/safety?make=Honda&model=Accord&year=2018" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "make": "HONDA",
  "model": "ACCORD",
  "year": 2018,
  "recalls": {
    "count": 1,
    "items": [{ "campaign": "18V268000", "component": "AIR BAGS", "summary": "..." }]
  },
  "ratings": { "overall": "5", "overall_front": "5", "overall_side": "5", "rollover": "4" }
}
GET/v1/vehicles/fuel-economy0.2 cr

MPG, fuel type, CO2 and estimated annual fuel cost for a year/make/model.

curl "https://salvagealert.com/api/v1/vehicles/fuel-economy?make=Toyota&model=Camry&year=2020" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "make": "TOYOTA",
  "model": "CAMRY",
  "year": 2020,
  "trims_available": 4,
  "trim": "Auto (S8)",
  "fuel_type": "Regular Gasoline",
  "mpg": { "city": 29, "highway": 41, "combined": 34 },
  "annual_fuel_cost_usd": 1450,
  "co2_g_per_mile": 261
}
GET/v1/fx0.2 cr

Daily reference exchange rates across the currencies SalvageAlert lots trade in. Defaults to a USD base against USD, EUR, CAD and GBP.

curl "https://salvagealert.com/api/v1/fx?base=USD&symbols=EUR,CAD,GBP" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "base": "USD",
  "date": "2026-05-30",
  "rates": { "EUR": 0.92, "CAD": 1.37, "GBP": 0.79 }
}

Market & fees

Aggregate price statistics and all-in buyer-fee estimates for every marketplace we cover.

GET/v1/stats/market1 cr

Aggregate price snapshot for a make (and optional model) across the whole archive: distinct lot count and the bid distribution (average, median, min, max) plus average pre-loss retail value, all normalized to USD.

curl "https://salvagealert.com/api/v1/stats/market?make=TOYOTA&model=COROLLA" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "make": "TOYOTA",
  "model": "COROLLA",
  "currency": "USD",
  "lot_count": 1284,
  "avg_bid": 5210,
  "median_bid": 4600,
  "min_bid": 150,
  "max_bid": 21400,
  "avg_retail_value": 14320
}
GET/v1/fees/estimate0.2 cr

All-in buyer-fee breakdown for a hammer price on any marketplace we cover (IAA CA/US/UK, Copart CA/US/UK/DE). Pure calculation, no scrape, with itemized fee lines plus optional PST/GST.

curl "https://salvagealert.com/api/v1/fees/estimate?source=iaai_ca&bid=5000&tax_region=QC" \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
ResponseJSON
{
  "source": "iaai_ca",
  "market": "iaa_ca",
  "currency": "CAD",
  "bid_amount": 5000,
  "fee_lines": [
    { "key": "buyerFee", "label": "Buyer fee", "amount": 550 },
    { "key": "internetBidFee", "label": "Internet bid fee", "amount": 79 },
    { "key": "gateFee", "label": "Gate fee", "amount": 79 },
    { "key": "environmentalFee", "label": "Environmental fee", "amount": 15 }
  ],
  "total_fees": 723,
  "pst": 72.12,
  "gst": 36.15,
  "tax_breakdown": [
    { "kind": "qst", "field": "pst", "label": "Quebec QST", "rate": 0.09975, "basis": "fees", "amount": 72.12 },
    { "kind": "gst", "field": "gst", "label": "GST", "rate": 0.05, "basis": "fees", "amount": 36.15 }
  ],
  "total_without_taxes": 5723,
  "total_with_taxes": 5831.27,
  "note": "IAA Canada Standard schedule"
}

Webhooks

Subscribe to events and receive HMAC-signed POST requests when new lots match your watches. Failed deliveries are retried on a fixed schedule: 1 min, 5 min, 30 min, 2 h, 8 h, then 24 h.

POST/v1/webhooks0

Create a webhook. The HMAC secret is returned exactly once.

curl -X POST https://salvagealert.com/api/v1/webhooks \
  -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/webhooks/salvagealert","events":["lot.matched"]}'

List, update, delete

curl https://salvagealert.com/api/v1/webhooks \
  -H "Authorization: Bearer $SALVAGEALERT_KEY"
POST/v1/webhooks/{id}/test0

Synchronous test delivery. Not persisted and not retried.

ResponseJSON
{"ok":true,"status":200,"body_preview":"OK"}
GET/v1/webhooks/{id}/deliveries0

Most recent deliveries with attempts, status, latency and redacted preview.

Query params: limit (1-200, default 50), status (pending, delivered, failed or dead).

Only successful response previews are stored. Failed responses get opaque tags so webhook receivers cannot become exfiltration channels.
POST/v1/watches0

A saved filter set that decides which new lots fire lot.matched.

Supported filters include keyword, make/model ids or names, year range, damage keywords, title types, buy-now, bid and odometer ranges.

curl https://salvagealert.com/api/v1/watches -H "Authorization: Bearer $SALVAGEALERT_KEY"
curl -X POST https://salvagealert.com/api/v1/watches -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source": "copart_us", "label": "Tacoma 2018+", "filters": {"make_names": ["TOYOTA"], "model_names": ["TACOMA"], "year_from": 2018}}'
curl -X PATCH https://salvagealert.com/api/v1/watches/wch_9f3cโ€ฆ -H "Authorization: Bearer $SALVAGEALERT_KEY" \
  -H "Content-Type: application/json" -d '{"active": false}'
curl -X DELETE https://salvagealert.com/api/v1/watches/wch_9f3cโ€ฆ -H "Authorization: Bearer $SALVAGEALERT_KEY"

lot.matched ยท lot.updated ยท lot.sold

Webhook payloads

Every delivery is a POST whose body is { event, created_at, data }. created_at is unix seconds. data is one of the five shapes below and always carries a kind discriminator โ€” switch on that, never on whether some field happens to be present. Two events are polymorphic: lot.matched arrives as watch_match or vin_watch_match (which carries vin_watch_id, not watch_id), and lot.updated as closing_soon, auction_result, relisted,rescheduled, bid_changed, max_bid_crossed or price_drop. The last three are the favorites alerts โ€” a bid jump in the 48 hours before a sale, the live bid passing the ceiling set on a lot, a Buy Now price reaching the target set on one.

eventkindwhen it firesfields beyond kind, match_id, search_id, source
lot.matchedwatch_matchA /watches filter set matched a newly ingested lot. Once per (watch, lot).watch_id, lot (the full Lot object)
lot.matchedvin_watch_matchA /vin-watches VIN was found in a source.vin_watch_id, lot
lot.updatedclosing_soonBid movement on a favorited lot in the approach window before its sale. Repeats while the price moves.closing_in_seconds, before, after
lot.updatedbid_changedA favorited lot's bid jumped by a meaningful step inside the 48h window.closing_in_seconds, before, after, currency
lot.updatedmax_bid_crossedThe live bid passed the ceiling set on a favorited lot. Once per ceiling.lot_key, max_bid_value, high_prebid_value, currency
lot.updatedprice_dropA favorited lot's Buy Now price met the target set on it. Once per target.lot_key, target_price, buy_now_value, high_prebid_value, currency
lot.updatedauction_resultThe sale ran and the lot settled: sold or unsold.result, final_price, before, after
lot.updatedrelistedThe sale ran and the same lot is running again on a new date. `won: true` when you had marked it won.relisted, sale_date_utc, won?
lot.updatedrescheduledA sale that had not run moved to another time, by an hour or more.rescheduled, sale_date_utc
lot.solddelistedThe lot left its source feed and we closed it out.lot_key, vin, year, make, model, final_bid, buy_now_value, currency, sold_at
webhook.testtestYou called POST /webhooks/{id}/test.โ€”
lot.matched โ€” kind: watch_matchJSON
{
  "event": "lot.matched",
  "created_at": 1757000000,
  "data": {
    "kind": "watch_match",
    "source": "copart_us",
    "watch_id": "wch_9f3c...",
    "lot": { "stockNum": "50567636", "vin": "1HGCY1F32RA******", "year": 2024, "...": "the full camelCase Lot object" }
  }
}
lot.matched โ€” kind: vin_watch_matchJSON
{
  "event": "lot.matched",
  "created_at": 1757000000,
  "data": {
    "kind": "vin_watch_match",
    "vin_watch_id": "vw_41ab...",
    "vin": "1HGCM82633A004352",
    "source": "iaai_us",
    "match_id": 918273,
    "lot": { "...": "the full camelCase Lot object" }
  }
}
lot.updated โ€” kind: closing_soonJSON
{
  "event": "lot.updated",
  "created_at": 1757000000,
  "data": {
    "kind": "closing_soon",
    "match_id": 918273,
    "search_id": 4412,
    "source": "copart_ca",
    "closing_in_seconds": 1740,
    "before": { "high_prebid_value": 4200, "buy_now_value": null },
    "after":  { "high_prebid_value": 4650, "buy_now_value": null }
  }
}
lot.updated โ€” kind: auction_resultJSON
{
  "event": "lot.updated",
  "created_at": 1757000000,
  "data": {
    "kind": "auction_result",
    "match_id": 918273,
    "search_id": 4412,
    "source": "copart_ca",
    "result": "sold",
    "final_price": 5100,
    "before": { "high_prebid_value": 4650, "buy_now_value": null },
    "after":  { "high_prebid_value": 5100, "buy_now_value": null }
  }
}
lot.updated โ€” kind: max_bid_crossedJSON
{
  "event": "lot.updated",
  "created_at": 1757000000,
  "data": {
    "kind": "max_bid_crossed",
    "match_id": 918273,
    "search_id": 4412,
    "source": "copart_us",
    "lot_key": "50567636",
    "max_bid_value": 4500,
    "high_prebid_value": 4650,
    "currency": "USD"
  }
}
lot.sold โ€” kind: delistedJSON
{
  "event": "lot.sold",
  "created_at": 1757000000,
  "data": {
    "kind": "delisted",
    "match_id": 918273,
    "search_id": 4412,
    "source": "iaai_ca",
    "lot_key": "50567636",
    "vin": "1HGCM82633A004352",
    "year": 2024, "make": "HONDA", "model": "ACCORD EX",
    "final_bid": 5100, "buy_now_value": null,
    "currency": "CAD",
    "sold_at": 1757000000
  }
}

Retries ยท auto-disable

Delivery lifecycle

A delivery is attempted up to 7 times โ€” after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours, roughly 35 hours end to end โ€” then marked dead and dropped. Any 2xx is success. A 4xx other than 408 or 429 is a permanent refusal and is not retried. Redirects are not followed, so a 3xx is a failure. The timeout is 15 seconds. Deliveries are not ordered and may arrive more than once: dedupe on X-Salvagealert-Delivery-Id, which is stable across retries of the same delivery.

After 20 consecutive failed deliveries the subscription itself is switched off โ€” active becomes false, anything still queued for it is dropped, and events that happen while it is off are never backfilled. The counter resets to 0 on any success, so it takes a sustained outage; but a receiver down for a long maintenance window will be disabled. We email the account owner when it happens. Re-enable with PATCH /v1/webhooks/{id} {"active": true} once your endpoint is answering, and check GET /v1/webhooks/{id}/deliveries to see what failed.
Only generic deliveries are signed. format is auto-detected from your URL โ€” a discord.com or hooks.slack.com URL is re-rendered into that serviceโ€™s own message shape and sent with no X-Salvagealert-Signature header at all, so the receiver cannot verify the delivery came from us and anyone who learns the URL can post to it. Send "format": "generic" explicitly if you want the HMAC.

Security

Verifying signatures

Every delivery includes X-Salvagealert-Signature. Compute HMAC_SHA256(secret, ts.raw_body), compare it with v1, and reject timestamps older than five minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(secret, rawBody, header) {
  const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header ?? '');
  if (!m) return false;
  const [, ts, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));
}

Lot object

Every endpoint that returns lots uses the same object shape. Null or missing values mean the source did not provide that field.

ResponseJSON
{
  "stockNum": "50567636",
  "vin": "1HGCY1F32RA******",
  "year": 2024,
  "make": "HONDA",
  "model": "ACCORD EX",
  "brand": "CLEAN TITLE",
  "odometerReading": "22215",
  "primaryDamage": "FRONT END",
  "estimatedRetailValue": 26125,
  "imageUrl": "https://cs.copart.com/..."
}