cypress.DOCS
Log inRegister
Cypress Docs
Developers

Offers

Read the offers on your vehicles and the offers you made, and accept, decline, counter or withdraw them with the app's own rules, dry runs and idempotency keys.

TL;DRAI summary

This chapter explains how to manage offers on your vehicles and the offers you make on others. You'll learn how to view, accept, decline, counter, or withdraw offers, while understanding the rules Cypress uses for these decisions.

Dive deeper:

The offers on your vehicles, and the offers your organization made on others', are on the API, and so are the decisions: accept, decline, counter, withdraw. Since 1.5.0. Every decision goes through the same offer commands the app and Cedar use, so the rules are the app's whoever calls:

  • The floor is the seller's consent. Accepting an offer below the floor, or countering below it, is a person's decision in the app. Every API caller is refused with 403 PEOPLE_ONLY: an API key, a connected app acting for a person, an agent. An agent cannot decide a below-floor offer at all, a decline included (403 BELOW_FLOOR).
  • Only an open offer can be decided (NEW, REVIEWED or COUNTERED). Anything else answers 409 CONFLICT, and nothing is written.
  • Your organization's switches hold. A member without the right to decide offers, through an app, is refused with 403 POLICY_DENIED.
  • The buyer stays anonymous, as in the webhooks: an offer never names the organization or the person who made it.
ScopeGrants
offers:readGET /api/v1/offers, GET /api/v1/offers/{id}
offers:writeEverything offers:read grants, plus the four decisions below

Both an API key and a Connect Cypress token are accepted. Agents reach the same commands through Cypress Operate (list_offers, get_offer, accept_offer, decline_offer, counter_offer, withdraw_offer).

The offer

{
  "id": "clx…",
  "href": "/api/v1/offers/clx…",
  "side": "SELLER",
  "vehicleId": "clx…",
  "vehicleHref": "/api/v1/vehicles/clx…",
  "vin": "1FTFW1ET5DFC10312",
  "amount": 21000,
  "status": "COUNTERED",
  "counterAmount": 22000,
  "counterExpiresAt": "2026-11-04T15:00:00.000Z",
  "createdOn": "2026-11-02T14:10:00.000Z",
  "updatedOn": "2026-11-02T15:00:00.000Z",
  "links": { "app": "https://app.getcypress.ai/go/offer/clx…" }
}

side is SELLER for an offer on one of your vehicles and BUYER for one your organization made. On the buyer side vehicleHref is null: the vehicle is not yours to read through the vehicles routes. status is one of NEW, REVIEWED, COUNTERED, ACCEPTED, REJECTED, EXPIRED, WITHDRAWN or CANCELLED. links.app opens the offer in Seller Hub for the seller and in Buyer Hub for the buyer.

List and read

GET /api/v1/offers returns the offers on your vehicles, newest first, 25 to a page (limit up to 100, offset, nextHref). Filters combine:

FilterExample
sideSELLER (the default) or BUYER, the offers you made
statusNEW,COUNTERED
vehicleIdthe offers on one vehicle
curl "$BASE/api/v1/offers?status=NEW,REVIEWED" -H "authorization: Bearer $TOKEN"

GET /api/v1/offers/{id} returns one offer on your vehicle or one you made. Any other offer answers 404, exactly as one that does not exist.

The buyer's record with you

GET /api/v1/offers/received (the Seller Hub's list, list_offers_received) carries a buyerRecord on each offer: what this buyer did with you, never with anyone else. null when the two of you never closed a deal. Since 1.9.0.

"buyerRecord": { "purchasesWithYou": 4, "lastPurchaseAt": "2026-09-30T16:20:00.000Z", "avgDaysToPay": 2.5 }
FieldMeaning
purchasesWithYouClosings with you that completed
lastPurchaseAtWhen the latest of them completed
avgDaysToPayAverage days from payment due (the Bill of Sale done) to the funds confirmed, one decimal; a closing recorded off-platform is left out. null when none has both

The condition at offer

Every offer made since 1.9.0 keeps the car's condition as the buyer saw it at that moment, so a later "it was not like this" has the record. Both sides read it as conditionAtOffer: the buyer on GET /api/v1/offers/made (list_offers_made), the seller on GET /api/v1/offers/received. null on an older offer.

"conditionAtOffer": {
  "condition": "GOOD", "conditionScore": 3.8, "arvisScore": 3.6, "damageCount": 3,
  "inspection": { "id": "clx…", "at": "2026-10-26T15:00:00.000Z", "provider": "MANHEIM", "grade": 3.5, "gradeLabel": "Average", "status": "REVIEW" },
  "takenAt": "2026-10-27T18:04:11.000Z"
}

inspection is the latest one the vehicle page shows (in review or finalized), damageCount its damages. Repair costs and the inspector's value estimate are the seller's and are never kept.

Which offer to answer first

GET /api/v1/offers/received does the arithmetic, so your app or agent reads the facts instead of working them out. Since 1.9.0. Each offer carries:

FieldMeaning
waitingHoursWhole hours since the offer was made, while it waits for your answer (NEW or REVIEWED). null once you answered it or it was decided
ofListingThe offer over your listing price, to three decimals: 0.962 is 96.2%. null without a listing price
underFloorBy, aboveFloorByHow far below or above your floor, in dollars. At the floor, aboveFloorBy is 0. Both are null with no floor
answerOrderIts place among the offers waiting for your answer: 1 is the one to answer first. null when it is not waiting for you
answerWhyWhy that place, in plain words

The order: offers at or above the floor first, then the one waiting longest, then the highest share of its listing price.

"answerOrder": 1,
"answerWhy": "above the floor by $600, waiting 20h, 96.2% of listing"

The answer also carries a summary of the offers waiting for your answer:

"summary": {
  "count": 4,
  "answerFirst": { "offerId": "clx…a", "why": "above the floor by $600, waiting 20h, 96.2% of listing" },
  "oldest": { "offerId": "clx…b", "waitingHours": 30 },
  "closestToListing": { "offerId": "clx…a", "ofListing": 0.962 },
  "underFloor": [{ "offerId": "clx…b", "by": 100 }, { "offerId": "clx…d", "by": 2000 }]
}

answerOrder and summary cover every offer waiting for your answer that search and vehicleId match, on every page, whatever the tab and sort. So the one to answer first is never only the first page's. Each names its offer by id: two offers on the same model stay apart.

On GET /api/v1/offers/made (list_offers_made), an offer with a live counter from the seller carries counterHoursLeft: the whole hours left to accept it, never below 0. It is null without a live counter. Since 1.9.0.

Decide

RequestBodyDoes
POST /api/v1/offers/{id}/acceptnoneAccepts an offer on your vehicle. The closing opens
POST /api/v1/offers/{id}/decline{ "reason"? } (up to 500 characters)Declines it; the buyer is told
POST /api/v1/offers/{id}/counter{ "amount", "message"? } (up to 1,000 characters)Answers with your price. The buyer has 48 hours (counterExpiresAt)
POST /api/v1/offers/{id}/withdrawnoneWithdraws an open offer your organization made. The vehicle goes back to Listed only when no other offer on it is still open

The answer is { "result": { "offerId", "status", "message" }, "offer": { … } }, the offer as it is after the decision. A counter's result also carries counterAmount and counterExpiresAt.

curl -X POST "$BASE/api/v1/offers/clx…/counter" \
  -H "authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -H "Idempotency-Key: counter-off_123-1" \
  -d '{ "amount": 22000, "message": "Fresh tires, one owner." }'

Dry runs. Every decision takes ?dryRun=true: the rules are checked and the answer is { "dryRun": true, "result": { "status": "WOULD_ACCEPT", … } }, with nothing written and no event sent. A refusal is the same refusal either way, so a dry run tells you whether the decision is yours to make.

Idempotency keys. Send Idempotency-Key (up to 200 characters) and a retry with the same key and body gets the first answer, with Idempotent-Replayed: true, instead of a second decision. The same key with a different body is 422 IDEMPOTENCY_KEY_REUSED; a key whose first request is still running answers 409 IDEMPOTENCY_IN_PROGRESS.

Decisions send the offer events a decision in the app sends, to the seller's endpoints: OFFERS.ACCEPTED, OFFERS.DECLINED and OFFERS.COUNTERED, and OFFERS.WITHDRAWN when a buyer withdraws. The codes are in Errors.

Picks that learn

Since 1.9.0. GET /api/v1/marketplace/picks (MCP: list_marketplace_picks) ranks the cars for sale like the ones your person made offers on and the ones your organization bought (a completed purchase counts twice an offer: the same make, model and body, in your price band), each with a plain reason ("Ford Transit: you bought 2 of these"). Picks are a person's: an API key is refused.

When a pick is not for you, say why:

POST /api/v1/marketplace/picks/{vehicleId}/dismiss
{ "reason": "PRICE", "note": "Over budget this quarter" }

reason is PRICE, MILEAGE, MODEL, LOCATION or OTHER (MCP: dismiss_marketplace_pick). The car never comes back in your organization's picks, and cars like it rank lower by the reason: as dear (PRICE), with as many miles (MILEAGE), the same model (MODEL), in the same state (LOCATION). Saying it again replaces the reason. DELETE on the same path (MCP: restore_marketplace_pick) takes it back. Only your organization reads it: the seller is never told.

Your usual

Since 1.9.0. GET /api/v1/marketplace/purchase-profile (MCP: get_purchase_profile, marketplace:read) reads what your organization typically buys on Cypress, from its own completed purchases (an accepted offer of yours on a car now sold) in the last year (days, 30 to 1,095): the price you paid, the age and mileage when bought (each the middle half, the whole range under four purchases, with the median), the model years that age reads as today, and the makes, models, bodyClasses and states you bought in, with cadenceDays and lastPurchaseAt. Use it to prefill a buy plan target. Only your own trades: no seller is named.

{
  "purchases": 12, "periodDays": 365,
  "price": { "low": 18000, "median": 20500, "high": 24000 },
  "age": { "low": 2, "median": 3, "high": 4 }, "years": { "min": 2022, "max": 2024 },
  "mileage": { "low": 30000, "median": 40000, "high": 60000 },
  "models": [{ "make": "Ford", "model": "Transit", "count": 7 }],
  "bodyClasses": [{ "label": "Van", "count": 11, "share": 0.92 }],
  "states": [{ "label": "TX", "count": 8, "share": 0.67 }],
  "words": "Your usual, from 12 purchases in the last 365 days: 2022–2024, Ford Transit, $18,000–$24,000, 30,000–60,000 miles, mostly bought in TX."
}