cypress.DOCS
Log inRegister
Cypress Docs
Developers

Pushing inventory

Send your vehicles one at a time or 500 at a time, map your feed's fields, attach an inspection report by its link, and keep the whole inventory in step.

TL;DRAI summary

This chapter explains how to push your vehicle inventory to Cypress, either one vehicle at a time or in bulk. You will learn how to add, update, or replace vehicles, and how to keep your Cypress inventory in sync with your system.

Dive deeper:

Your system is the source of truth for what you own; Cypress keeps a copy it can sell. A push writes that copy: one vehicle with POST /api/v1/vehicles, or up to 500 with POST /api/v1/vehicles/bulk. Both need the vehicles scope.

One vehicle or many

You want to…CallWhat happens
Add a vehiclePOST /api/v1/vehiclesCreated as a draft (201). A VIN already in your inventory answers 409 with its id
Add it or replace it, by VINPOST /api/v1/vehicles?mode=upsertNew VINs are created (201), known ones replaced (200): every field you leave out is cleared
Add it or update what you send, by VINPOST /api/v1/vehicles?mode=mergeNew VINs are created (201); on known ones only the fields you send change (200) (since 1.8.0)
Replace one you knowPUT /api/v1/vehicles/{id}Every field you leave out is cleared (see below)
Change a few fieldsPATCH /api/v1/vehicles/{id}Only the fields you send change
Send manyPOST /api/v1/vehicles/bulk (?mode=merge or ?mode=upsert)Up to 500 items, an answer per item; one bad item never fails the others
Send your whole inventory, and take off what leftPOST /api/v1/vehicles/bulk?mode=merge&removeMissing=trueThe vehicles the push leaves out are archived, at once or, when a buyer is involved, after Cypress's review: see below (since 1.8.0)

A daily feed usually sends everything with POST /api/v1/vehicles/bulk?mode=merge: new vehicles are added, the rest are brought up to date, and you never need to know which is which. Merge or upsert? merge changes only the fields each item sends, so the details your feed doesn't carry (a trim, a transmission, an interior color your team or Cypress filled in) stay. upsert makes each vehicle exactly what you send and clears the rest: use it when your feed carries every field. Every push takes ?dryRun=true (every check, nothing written); the single-vehicle writes also take an Idempotency-Key.

A bulk push answers one item per vehicle, in the order sent:

{
  "items": [
    { "vin": "1FTFW1ET5DFC10312", "status": 201, "id": "clx…", "href": "/api/v1/vehicles/clx…", "inspectionReport": "QUEUED" },
    { "vin": "1FAHP2E88DG100002", "status": 200, "id": "clx…", "href": "/api/v1/vehicles/clx…" },
    { "vin": "BAD", "status": 422, "errors": ["vin: …"] }
  ],
  "created": 1, "updated": 1, "conflicts": 0, "invalid": 1, "refused": 0
}

The fields

Four fields are required: vin, modelYear, make and model. Cypress does not decode the VIN of a pushed vehicle: your data is the record.

FieldWhat it is
vin, modelYear, make, modelThe vehicle. model may carry the trim ("228I AWD"); a known trim is lifted into trim
trim, bodyClass, driveType, transmission, fuelTypeOptional specs
exteriorColor, interiorColorAs your system names them
mileageWhole miles
conditionPreferably the industry 1.0–5.0 grade (3.9); EXCELLENT, GOOD, FAIR or POOR also work
stockNumberYour unit or stock number, shown to people
externalIdsYour systems' ids for the vehicle, to look it up by them: see Your own ids
locationname, address, city, state, zip. A yard code such as "DEN" goes in name: when it matches one of your saved locations (Settings › Locations, any case), Cypress fills the address, city, state and ZIP you leave out (since 1.8.0). Since 1.9.0 a location's aliases match too ("HOU-2" for "Houston Hobby lot"), and the vehicle takes the location's own name
floorPriceYour floor: the lowest sale price you accept. Set at once on a vehicle not yet on the market; on one on the market or in a sale, a change Cypress reviews first (since 1.8.0)
imagesUp to 50 { "href", "type" } links Cypress can fetch; the first, or the one typed PRIMARY_IMAGE, is the cover
inspectionReportUrlA third-party inspection report's link: see Inspection reports (since 1.8.0)
listingPut it on the market in the same push: prices, channels and listingMode. See Pricing & listing rules
lifeStageOPERATION or PLANNING sends it to the plan instead of to market: see Planning API

A field Cypress doesn't know is kept with the push and never refused, so your feed can send its own columns as they are.

A typical fleet feed maps like this:

{
  "vin": "1FTFW1ET5DFC10312",
  "modelYear": 2013, "make": "Ford", "model": "F-150 XLT",
  "exteriorColor": "BLACK",
  "mileage": 88500,
  "stockNumber": "101815",
  "externalIds": [{ "source": "fleet", "id": "101815" }],
  "location": { "name": "DEN" },
  "floorPrice": 24300,
  "inspectionReportUrl": "https://inspectionreport.manheim.com/?disclosureid=…"
}

With a saved location named DEN, the vehicle gets its address. A name that matches none of your saved locations is a warning on the item, and the vehicle keeps what you sent. The item says what became of the floor in floorPrice: SET, UNCHANGED, or STAGED when the vehicle is on the market and Cypress reviews the change first.

What a replace clears. A PUT, or mode=upsert on a VIN you already pushed, replaces the vehicle: a field you leave out is cleared, and the pictures you pushed are replaced by the ones you send (none sent, none kept). Two things a replace never touches: pictures added in the app or read from an inspection report, and — since 1.8.0 — a condition an inspection set, when the push carries no condition. A PATCH, or mode=merge, changes only what it names: your pictures stay unless it sends images.

Prices on a re-push. The floor travels on every push (floorPrice, above). The listing block creates the listing the first time; once a vehicle has an open listing, a re-push leaves the listing's own prices as they are: change them with PATCH /api/v1/listings/{id} (the vehicle's read names the listing in listing.id). A push that sends both floorPrice and listing.floorPrice must send the same amount.

Inspection reports

Since 1.9.0. A vehicle's read also has its latest ARVIS inspection, Cypress's own, in arvisInspection: its status, the technician's grade (EXCELLENT, GOOD, FAIR, POOR) with the same score the app shows (5 to 2), when it was inspected, how many damages it found and their estimated repair cost, and a url that opens it in ARVIS through the app's sign-on (for a person signed in to Cypress whose role may see ARVIS). It is null when the vehicle has none. The third-party report below is separate, in inspectionReport.

Since 1.8.0. Send the link of a third-party inspection report with the vehicle, and Cypress reads the report for you. Today that is the Manheim condition report (https://inspectionreport.manheim.com/?disclosureid=…):

  • its grade becomes the vehicle's condition (unless a Cypress inspection was finalized for it),
  • each damage is recorded, with the title, keys, tires and warning lights,
  • its photos are added to the vehicle, so a feed with no pictures still lists with them.

The push answers right away and the report is read in the background. The push says what it did with the link, in inspectionReport:

AnswerMeans
QUEUEDA new link: Cypress is reading it
UNCHANGEDThe vehicle already has this link: nothing to do
LINK_ONLYNo provider Cypress reads: the link is kept on the vehicle for people to open

A Manheim link that names no report (no disclosureid) is a warning on the push, and the vehicle is written anyway. Because the same link changes nothing, a daily feed can send the link every time; a new link replaces the report, and a link whose report could not be read is tried again at most once a day. Null or no inspectionReportUrl leaves the vehicle's report as it is.

When the report lands, your endpoint hears VEHICLES.UPDATED with changedFields: ["inspectionReport"] and the report in inspectionReport. Its status is IMPORTING_PHOTOS when the grade and damages are in and the photos are on their way, COMPLETE when everything is in, FAILED when the report could not be read.

The event and the vehicle's read carry the report in the same shape:

"inspectionReport": {
  "url": "https://inspectionreport.manheim.com/?disclosureid=…",
  "provider": "MANHEIM",
  "status": "COMPLETE",
  "grade": 3.9, "gradeLabel": "Average",
  "damages": 3, "photos": 37,
  "updatedOn": "2026-10-22T14:02:11.000Z"
}

and GET /api/v1/vehicles/{id}/history-report has each damage. To attach a link to a vehicle on its own, POST /api/v1/vehicles/{id}/history-report with { "url": "…" } (MCP: attach_vehicle_history_report).

Keys and declared damage

Since 1.9.0. What the seller says of a car travels with it, apart from any inspection:

  • Keys and a damage note. keyCount (how many keys come with the car, 0 for none) and damageNote (the seller's own words) are set when the car is added (POST /api/v1/fleet/vehicles, add_vehicle) or edited (PATCH /api/v1/fleet/vehicles/{id}, edit_vehicle); null clears them.
  • Damage lines. POST /api/v1/vehicles/{id}/damages (add_vehicle_damage) declares one: area (rear bumper, driver door), description, severity (MINOR, MODERATE, SEVERE, CRITICAL) and, optionally, a picture sent as contentBase64 and contentType, as a photo upload is. The picture joins the car's photos as a damage photo, never as the cover. GET on the same path lists the lines with each picture's link; DELETE …/damages/{damageId} takes one back. Declaring damage is a person's act: an agent's token is refused 403 PEOPLE_ONLY.

The vehicle's read carries keyCount, damageNote and damages. A change to a car's condition, grade, keys, damage note or damage lines is told on the car's Updates to the seller, to Cypress and to every buyer with an open offer on it, who bid on the car as it was described; your endpoint hears VEHICLES.UPDATED with changedFields naming what moved (damages, keyCount, damageNote, condition).

Condition checked in person

Since 1.10.0. A condition can be a guess, a grade from a feed, or what a person saw. edit_vehicle takes conditionChecked: true when someone at the organization looked at the car and its condition is as set: Cypress keeps who and when, and the vehicle's read carries conditionCheck (checkedOn, by), or null when nobody has. false clears it. It is a person's word: an agent's token is refused 403 PEOPLE_ONLY. Listing readiness warns (field: "conditionCheck") when a car has a condition nobody checked and no inspection; it never blocks a listing.

Keeping the whole inventory in step

Since 1.8.0. Add removeMissing=true and the push is your whole inventory: every active vehicle Cypress holds for you that the push leaves out is one you want off the platform. You don't keep track of what you sent or what left; the next push says it.

POST /api/v1/vehicles/bulk?mode=merge&removeMissing=true

What happens to a vehicle the push leaves out is what happens when you archive it in the app, or pick the "vehicles not in this file" on the Import screen:

  • With no open offer and no sale in progress, it is archived at once.
  • With an open offer, or a sale in progress on Cypress (an accepted offer, due diligence, ready to deliver), it waits for a Cypress Manager, flagged with liveDeal so the reviewer sees the deal. A vehicle on the market pauses while it waits (ON_HOLD); if the removal is rejected, it goes back on the market.
  • A vehicle whose removal Cypress rejected is not asked again on every push. Push it again and leave it out later, and it is filed again.
  • Active means not sold, not archived and not in Planning, whoever added it: a vehicle added in the app counts too.
  • A vehicle that already has a request waiting is not filed twice (alreadyPending).
  • An item refused for another reason (a bad field, an id another vehicle holds) still counts as in your inventory: a typo never takes a car off.
  • A vehicle you push that still has a request waiting (it was left out before) gets a warning on its item. The push does not withdraw the request; tell Cypress if the vehicle should stay. A vehicle you push that is already archived gets its details updated, and a warning: a push does not bring it back to Cypress.

The answer adds removals: one entry in items per vehicle taken off or filed, and one in kept per vehicle left out but not filed, with the reason (REVIEW_REJECTED, or NO_LONGER_ACTIVE for one sold or archived while the push was written):

"removals": {
  "activeInventory": 120, "missing": 3, "archived": 1, "staged": 1, "alreadyPending": 0,
  "items": [
    { "vin": "…", "id": "clx…", "requestId": "clx…", "archived": true, "statusBefore": "LISTED", "statusAfter": "ARCHIVED",
      "paused": false, "alreadyPending": false, "liveDeal": null },
    { "vin": "…", "id": "clx…", "requestId": "clx…", "archived": false, "statusBefore": "UNDER_OFFER", "statusAfter": "UNDER_OFFER",
      "paused": false, "alreadyPending": false, "liveDeal": { "kind": "OFFERS", "openOffers": 1, "topOffer": 21000 } }
  ],
  "kept": [
    { "vin": "…", "id": "clx…", "status": "APPROVED", "reason": "REVIEW_REJECTED" }
  ]
}

Your endpoint hears the same through webhooks:

  • A vehicle archived at once: VEHICLES.DISPOSITION.RESOLVED right away, with decision: "APPROVED", automatic: true, statusBefore and statusAfter: "ARCHIVED".
  • A vehicle that waits for review: VEHICLES.DISPOSITION.STAGED now (its requestId, statusBefore, statusAfter, paused, reviewReason and liveDeal), then VEHICLES.DISPOSITION.RESOLVED when Cypress decides: the decision (APPROVED or REJECTED) and statusAfter: ARCHIVED when approved; when rejected, LISTED again if the request had paused it, otherwise the status it had.
  • Both carry source: "api-sync".

Add syncId=<your label> (letters, digits, ., :, - or _, up to 64) and the push's answer and both events carry it, so your logs can tie a decision to the push that asked for it.

More than 500 vehicles. Send the pages as usual, without the flag. On the last page, add removeMissing=true and inventoryVins: every VIN you hold (up to 20,000). Cypress compares against that list and the page's own vehicles. Nothing is kept between calls, so a page that failed is simply sent again.

POST /api/v1/vehicles/bulk?mode=merge&removeMissing=true&syncId=feed-2026-10-22
{ "vehicles": [ …the last page… ], "inventoryVins": ["1FTFW1ET5DFC10312", "…"] }

A truncated feed never empties your fleet. A push that leaves out more than a fifth of your active vehicles (and more than five) is refused whole, items included (409 MASS_REMOVAL, with activeInventory and missing): a vehicle with no buyer is archived at once, so the guard stops a broken feed before anything is written. When the removal is meant, add allowMassRemoval=true.

Try it first. With dryRun=true the push answers what each item would do and what would happen to each vehicle it leaves out, writing nothing.

To take off a single vehicle, or to say why it left (sold elsewhere, back on rent), the calls of De-listing remain.

How each car is doing

Since 1.9.0. Every vehicle in GET /api/v1/vehicles (and GET /api/v1/vehicles/{id}) says how buyers are taking to it, as the Seller Hub's Market column does:

FieldMeaning
engagementWhile the car is on the market (LISTED, UNDER_OFFER, ON_HOLD): COLD with no views, saves or offers in 14 days, HOT when the last 7 days beat the 7 before by 20% or more (or a new listing's first week has any), else WARM. Null off the market
listedDaysWhole days on the market from the listing date, stopped at the sale; null when it was never listed
views14d, saves14d, offers14dBuyer views, saves and offers in the last 14 days

Add engagement=COLD (or a comma list, HOT,WARM) to list only the cars on the market with that engagement, newest first: GET /api/v1/vehicles?engagement=COLD is the cars nobody looks at. It combines with the other filters, and totalCount and nextHref count the filtered set.

Every car on the marketplace says it too, whoever sells it: engagement is in each result of GET /api/v1/marketplace/vehicles (MCP: search_marketplace) and in GET /api/v1/marketplace/vehicles/{id} (get_marketplace_vehicle), so a buyer sees which cars are drawing interest. Another seller's car gives the level only: its views, saves and offers stay its seller's.

Dashboard numbers

Since 1.9.0. GET /api/v1/fleet/metrics?period=30d (MCP: get_fleet_metrics, scope vehicles) answers the dashboard's tiles from the same computation the dashboard page draws, so an app that shows them shows the dashboard's numbers. period is 7d, 30d (the default) or 90d, as the dashboard's switch (365d is read too).

{
  "period": "30d", "periodDays": 30,
  "from": "2026-09-27T21:00:00.000Z", "to": "2026-10-27T21:00:00.000Z",
  "pipeline": { "activePipeline": 69, "stuckCount": 33, "totalFleet": 104 },
  "tiles": [
    { "key": "activePipeline", "label": "Active pipeline", "value": 69, "display": "69", "format": "number",
      "snapshot": true, "prev": null, "delta": null, "lowerIsBetter": false, "trend": [], "trendStep": null },
    { "key": "wholesaleGmv", "label": "Wholesale GMV", "value": 1033400, "display": "$1.0M", "format": "currency",
      "snapshot": true, "prev": null, "delta": null, "lowerIsBetter": false, "trend": [], "trendStep": null },
    { "key": "offersPerListing", "label": "Offers per listing", "value": 0.3, "display": "0.3", "format": "decimal",
      "snapshot": false, "prev": 0.1, "delta": { "pct": 200, "better": true, "display": "↑200.0%" },
      "lowerIsBetter": false, "trend": [], "trendStep": null }
  ]
}

(Three of the seven tiles shown.) The tiles come in the dashboard's order: Active pipeline, Stuck 7+ days, Wholesale GMV, Retail value, Vehicles sold, Avg days to sell, Offers per listing.

FieldMeaning
label, displayThe dashboard's words and the value as it shows it ($1.0M, 122.4d, 0.3, US format). Show display as is to match it
snapshotTrue for what is true now: Active pipeline and Stuck 7+ days (the pipeline's counts, planning stock included) and Wholesale GMV and Retail value (the latest valuation of every vehicle not sold, archived or planning stock). A snapshot has no prior period, no delta and no trend
prev, deltaThe prior period of the same length, and the change against it. delta is null for a snapshot and when the prior period is 0; its display arrow means better (↑) or worse (↓), so a shorter Avg days to sell is ↑
trend, trendStepA real series only: Vehicles sold per day, Avg days to sell per week (a week without a sale reads as the period's average). The dashboard draws it when it has more than 2 points

There is no history of fleet value. fleetValue and retailValue, kept from before 1.9.0 beside vehiclesSold, avgDaysToSell, offersPerListing and daysToSellDistribution, are snapshots: prev is 0 and trend is empty. Never draw a fleet-value trend or compare it with a prior period. A sale counts on the day the vehicle became SOLD.

Comparable sales

Since 1.9.0. GET /api/v1/vehicles/{id}/comps (MCP: get_vehicle_comps, scope vehicles) reads what cars like one of yours sold for on Cypress: the same year (one either side), make and model, at a similar mileage (within 30%, never closer than 25,000 miles; a sale with no mileage on record counts), sold in the last year (days, 30 to 730).

  • Your own sales come one by one in yours.items, newest first, each with its price, asking price, mileage, date, state and buyer.
  • Other sellers' sales come only as a range in market: low, middle and high (rounded to $100), what they fetched against their ask, and how many. A range needs five sales from at least two other companies; with fewer, market says enough: false and gives no figure. Another seller's sale is never listed, priced on its own or named.
  • summary counts your sales and, when the range stands, theirs: the median sold price, that median as a percent of this car's listing price (pctOfListing) and what the sales fetched against their own ask (medianPctOfAsk).
  • blackBook is the car's latest Black Book value (retail, wholesale, trade-in), null when it has none.
{
  "summary": { "count": 7, "medianSoldPrice": 28000, "pctOfListing": 93.3, "medianPctOfAsk": 96.6, "basis": "YOURS_AND_MARKET",
    "words": "7 sold like it in the last 365 days, middle $28,000, 93% of your listing price." },
  "yours": { "totalCount": 2, "items": [ { "vehicleId": "clx…", "year": 2022, "make": "Ford", "model": "Transit", "mileage": 45000,
    "soldPrice": 29000, "askingPrice": 30000, "pctOfAsk": 96.7, "soldAt": "2026-10-22T12:09:50.891Z", "state": "TX",
    "buyer": { "organizationId": "clx…", "name": "Lakeside Motors" } } ] },
  "market": { "enough": true, "count": 5, "low": 27000, "median": 28000, "high": 29000, "medianPctOfAsk": 96.6, "medianMileage": null },
  "blackBook": { "source": "BLACK_BOOK", "retail": 32000, "wholesale": 27000, "tradeIn": 25000, "mileageAtValuation": 40000, "valuedAt": "…" }
}

GET /api/v1/vehicles/comps?year=2021&make=Ford&model=Transit (MCP: find_comparable_sales) asks the same for a car you name rather than one you hold; add mileage and listingPrice to compare with them. mine=true reads your own sales only, so mine=true&limit=1 answers "my last sold 2021 Ford Transit".

Staging an import for review

Since 1.9.0. A spreadsheet import (POST /api/v1/imports, import_vehicles) can be held for a person to review before it runs. Send it with "mode": "stage": Cypress runs the import's dry run, keeps it with the rows as parsed, and answers the staged batch (id, totalRows, exceptionRows). No vehicle is written, and your endpoint hears VEHICLES.IMPORT.STAGED.

GET /api/v1/imports/{batchId} reads it back (kind: "STAGED"): every row, what the dry run said of it, and its exceptions, the things a person should look at before it runs:

ExceptionMeans
DUPLICATE_VINThe VIN is on more than one row of the file
MILEAGE_DROPThe odometer is 10 times lower (or more) than the car's
UNKNOWN_LOCATIONThe location name matches no saved location and no alias
ALREADY_SOLDThe VIN is a car you already sold
ARCHIVE_LIVE_DEALThe row archives a car that has an open offer or a sale in progress
MISSING_LIVE_DEALA car the file leaves out has an open offer or a sale in progress (a row of its own, after the file's)

PATCH /api/v1/imports/{batchId}/rows/{rowId} (decide_import_row) settles a row: decision KEEP (do what the row proposes) or SKIP (leave it out), locationId to map an unknown location name to a saved location (add rememberAlias: true and the name becomes that location's alias, so the next file needs no decision), or mileage to correct the odometer.

POST /api/v1/imports/{batchId}/apply (apply_import) runs it once every exception is settled (409 CONFLICT lists the rows that are not), with the rows as decided: the import itself runs, with its checks, and answers the batches it started in importBatchIds; your endpoint hears VEHICLES.IMPORT.APPLIED, then VEHICLES.IMPORT.COMPLETED for each batch. ?dryRun=true answers the import's own dry run of the decided rows. Staging, deciding and applying are a person's acts: an agent's token is refused 403 PEOPLE_ONLY.

Location aliases. A saved location answers to other names your files use for it: PATCH /api/v1/locations/{locationId} with { "aliases": ["HOU-2", "Hobby"] } (the list replaces the one stored), read back on GET /api/v1/locations. An import row or a push naming an alias is put at that location, under the location's own name. A name another location already answers to is refused (409 CONFLICT). Aliases are set by a person.