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 you can manage offers on your vehicles, or offers you've made, using the Cypress platform. You can accept, decline, counter, or withdraw offers, but certain rules and permissions apply to protect your decisions and data.

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.

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.