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 add or update vehicles in Cypress from your own system, one by one or 500 at a time, and how a daily feed updates only the fields it sends. It also covers your floor price, saved locations, an inspection report link so Cypress pulls the condition and photos, and pushing your whole inventory so the vehicles you no longer hold are taken off.
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… | Call | What happens |
|---|---|---|
| Add a vehicle | POST /api/v1/vehicles | Created as a draft (201). A VIN already in your inventory answers 409 with its id |
| Add it or replace it, by VIN | POST /api/v1/vehicles?mode=upsert | New VINs are created (201), known ones replaced (200): every field you leave out is cleared |
| Add it or update what you send, by VIN | POST /api/v1/vehicles?mode=merge | New VINs are created (201); on known ones only the fields you send change (200) (since 1.8.0) |
| Replace one you know | PUT /api/v1/vehicles/{id} | Every field you leave out is cleared (see below) |
| Change a few fields | PATCH /api/v1/vehicles/{id} | Only the fields you send change |
| Send many | POST /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 left | POST /api/v1/vehicles/bulk?mode=merge&removeMissing=true | The 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.
| Field | What it is |
|---|---|
vin, modelYear, make, model | The vehicle. model may carry the trim ("228I AWD"); a known trim is lifted into trim |
trim, bodyClass, driveType, transmission, fuelType | Optional specs |
exteriorColor, interiorColor | As your system names them |
mileage | Whole miles |
condition | Preferably the industry 1.0–5.0 grade (3.9); EXCELLENT, GOOD, FAIR or POOR also work |
stockNumber | Your unit or stock number, shown to people |
externalIds | Your systems' ids for the vehicle, to look it up by them: see Your own ids |
location | name, 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) |
floorPrice | Your 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) |
images | Up to 50 { "href", "type" } links Cypress can fetch; the first, or the one typed PRIMARY_IMAGE, is the cover |
inspectionReportUrl | A third-party inspection report's link: see Inspection reports (since 1.8.0) |
listing | Put it on the market in the same push: prices, channels and listingMode. See Pricing & listing rules |
lifeStage | OPERATION 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.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:
| Answer | Means |
|---|---|
QUEUED | A new link: Cypress is reading it |
UNCHANGED | The vehicle already has this link: nothing to do |
LINK_ONLY | No 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).
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=trueWhat 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
liveDealso 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.RESOLVEDright away, withdecision: "APPROVED",automatic: true,statusBeforeandstatusAfter: "ARCHIVED". - A vehicle that waits for review:
VEHICLES.DISPOSITION.STAGEDnow (itsrequestId,statusBefore,statusAfter,paused,reviewReasonandliveDeal), thenVEHICLES.DISPOSITION.RESOLVEDwhen Cypress decides: thedecision(APPROVEDorREJECTED) andstatusAfter:ARCHIVEDwhen approved; when rejected,LISTEDagain 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.