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.
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,REVIEWEDorCOUNTERED). Anything else answers409 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.
| Scope | Grants |
|---|---|
offers:read | GET /api/v1/offers, GET /api/v1/offers/{id} |
offers:write | Everything 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:
| Filter | Example |
|---|---|
side | SELLER (the default) or BUYER, the offers you made |
status | NEW,COUNTERED |
vehicleId | the 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
| Request | Body | Does |
|---|---|---|
POST /api/v1/offers/{id}/accept | none | Accepts 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}/withdraw | none | Withdraws 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.