Errors
One error shape on both surfaces — what each status means, the stable codes to switch on, and whether to retry or fix.
TL;DRAI summary
This chapter explains the error messages you might see from Cypress, what they mean, and what to do next. You will learn how to identify different error types, such as problems with your request, permissions, or conflicts with a vehicle's status.
Every refusal on both surfaces (/api/v1 and /api/compatible/v1) has the
same JSON body:
{
"error": "This vehicle is in Planning, not in Sell. Updates, re-pushes, prices, listings, deletion and sold-elsewhere apply once its sale window hands it to Sell.",
"code": "PLANNING_STOCK"
}| Field | Always present | Meaning |
|---|---|---|
error | yes | A human-readable sentence for your logs. It names the first failing field on a 422. Don't parse it — the wording can improve between releases. |
code | on refusals that have one | A stable machine code. Switch on this, not on error. |
details | on 422, and on the codes that say so | On 422, the full validation result: fieldErrors (per field) and formErrors (whole payload). QUARTER_CLOSED names the vehicles it blocks and their quarter (vehicleIds, quarter); ODOMETER_ROLLBACK names the reading it conflicts with (recorded). |
Bulk endpoints never fail the whole batch for one bad item. They answer
200 with one result per item, and each failed item carries its own
status, code (when it has one) and errors[]:
{
"items": [
{ "vin": "1FTFW1ET5DFC10312", "status": 409, "code": "PLANNING_STOCK", "errors": ["This vehicle is in Planning, not in Sell. …"] },
{ "vin": "2FTFW1ET5DFC10313", "status": 201, "id": "clx…", "href": "/api/v1/vehicles/clx…" },
{ "vin": "3FTFW1ET5DFC10314", "status": 403, "code": "PLANNING_NOT_ENABLED", "errors": ["…"] }
],
"created": 1, "updated": 0, "conflicts": 1, "invalid": 0, "refused": 1
}refused counts the items that answered 403 — planning stock on an
organization without Planning, an embedded listing without the listings
scope, or a planning item without planning:write. It's additive: existing counters
(created/updated/conflicts/invalid) are unchanged, on both
POST /api/v1/vehicles/bulk and POST /api/compatible/v1/units/bulk.
Status codes
| Status | Means | Typical causes | Retry? |
|---|---|---|---|
400 | The body isn't JSON | Truncated or non-JSON body | No — fix the request |
401 | No valid key | Missing or malformed x-api-key; the key was revoked | No — check the key |
403 | The key can't do this | ORG_NOT_ACTIVE (organization not active), ACCOUNT_RULES (a feature off for your account, or one of its caps used), SCOPE_MISSING (key lacks the scope), PLANNING_NOT_ENABLED (planning capability, organization has no Planning), BUY_PLANNING_NOT_ENABLED (a Buy planning route, organization has no Buy planning), or AUTOMATIONS_NOT_ENABLED (an automations route, organization has no Automations) | No — ask your admin or Cypress; see Your organization and key |
404 | Not found for this key | Wrong id; an id from another organization; a vehicle in Operation or Planning (hidden from the API without a planning:read key) | No |
409 | Conflicts with the current state | Duplicate VIN; VIN change on PUT; an open listing already exists; the listing is closed; a deal is in progress; the vehicle is sold; PLANNING_STOCK; LIFE_STAGE_READ_ONLY; a quarter that is over (QUARTER_CLOSED); the plan codes below | No — read the resource and decide. Except PLAN_BUSY: retry after the Retry-After seconds |
422 | The payload is invalid | A missing required field, a wrong type, an out-of-range value, an unknown/lowercase/empty lifeStage, or CHANNEL_NOT_GRANTED (a channel your organization has no grant for) | No — fix the field named in error |
429 | Rate limit | More than 300 requests a minute on one key | Yes — after the Retry-After seconds |
5xx | Our side | — | Yes — with backoff; writes are safe to repeat with ?mode=upsert |
202 is not an error: it means the request was staged for Cypress review
(sold-elsewhere, or a removal while a deal is in progress). The decision
arrives later as a webhook — see De-listing.
Codes
| Code | Status | When | What to do |
|---|---|---|---|
PLANNING_STOCK | 409 | The vehicle is in Operation or Planning (lifeStage OPERATION or PLANNING), the organization has Planning, and you asked for something only Sell stock can do: an update, a re-push by VIN, prices, a listing, deletion, sold-elsewhere | Nothing, usually. The vehicle appears in the API on its own when its sale window hands it to Sell. If you believe it should be selling now, ask the fleet's planner. |
LIFE_STAGE_READ_ONLY | 409 | A PATCH/PUT sent a lifeStage on the other side of the Planning/Sell line (e.g. OPERATION for a vehicle in Sell) | Drop lifeStage from writes, or send back the value you read. A vehicle moves between Planning and Sell only through its plan. |
ORG_NOT_ACTIVE | 403 | Your organization's status isn't ACTIVE | Nothing you can fix from the API. Ask Cypress. GET /api/v1/organization still answers 200 and names this on every capability. |
ACCOUNT_RULES | 403 | Your account's rules refuse it (account on GET /api/v1/organization): a feature that is off for your organization (account.features: the team, apps, API keys, pricing rules, marketing campaigns), one of its caps used (account.limits against account.used: valuations and cars listed in total, team seats, campaign emails this month), or an individual's email not confirmed yet. Since 1.9.0. | Nothing from the API. Confirm the email, or ask Cypress: it sets the rules for each kind of account and for your organization. |
PLANNING_NOT_ENABLED | 403 | A capability that needs Planning (planning stock, planning:read/planning:write) on an organization that doesn't have Planning switched on — including a vehicle in Operation or Planning when the organization has no Planning | Ask Cypress to turn on Planning for your organization. |
BUY_PLANNING_NOT_ENABLED | 403 | A Buy planning route (/api/v1/buy-planning/*, scopes buy-planning:read/buy-planning:write) on an organization that doesn't have Buy planning switched on. A switch of its own: Planning being on does not open it. Since 1.4.0. | Ask Cypress to turn on Buy planning for your organization. |
AUTOMATIONS_NOT_ENABLED | 403 | An automations route (/api/v1/automations, scopes automations:read/automations:write) on an organization that doesn't have Automations switched on. Asked before the scopes. Since 1.5.0. | Ask Cypress to turn on Automations for your organization. |
SCOPE_MISSING | 403 | The key doesn't hold the scope this endpoint needs | Ask your organization's admin to add the scope on the Integrations page — the key doesn't need to be rotated. |
CHANNEL_NOT_GRANTED | 422 | A listing/offering create or update named a sales channel (MARKETPLACE, STOREFRONT, DIRECT_LINK) your organization has no grant for | Drop the channel, or ask Cypress for the grant. |
Offers, listings, external ids and retries (1.5.0)
The offer and listing commands answer the same codes to every caller: an integration, a connected app or an agent. A decision that is refused writes nothing.
| Code | Status | When | What to do |
|---|---|---|---|
EXTERNAL_ID_TAKEN | 409 | An externalIds entry (source and id) already names another record of the same kind in your organization: a vehicle, listing, contact, business, deal or task. taken names it. On a bulk push, only that item is refused | Look the id up (?externalId=<source>:<id> on the list) and fix the mapping on your side. See Your own ids. |
PEOPLE_ONLY | 403 | Only a person may do this, and nothing is written. From any caller of the API: accepting or countering an offer below the floor, and a campaign audience of pasted addresses (email-list). From an agent or an API key: emailing a contact one to one. From an agent: setting the floor (on a listing it changes or creates); cancelling a listing (taking a car off market: an agent pauses it instead); approving a quarter, Sell or Buy (including POST /api/v1/buy-planning/drafts/apply with approve: true); confirming that a closing's money arrived; activating or sending a campaign; switching an automation back on; reading or setting notification preferences | Leave it to a person in the app. An agent on Cypress Operate is given the link where a person decides. |
BELOW_FLOOR | 403 | A price or a decision would sit below the floor (a buy-now under the floor; a delegated decision on a below-floor offer) | Choose a value at or above the floor, or leave it to the seller's Fleet Manager. |
POLICY_DENIED | 403 | A rule or a delegation switch refuses this caller (for example, the organization has not delegated offer decisions) | Ask the organization's admin. |
CONFLICT | 409 | The record is not in a state that allows it: an offer that is no longer open, a price change under a live offer or sale | Read the record again and decide. |
NOT_FOUND | 404 | No such offer or listing for this key | Check the id. |
INVALID | 422 | The request is not valid for this command (for example, an Idempotency-Key longer than 200 characters) | Fix the request. |
IDEMPOTENCY_KEY_REUSED | 422 | The Idempotency-Key was already used with a different request | Use a new key for a new request. |
IDEMPOTENCY_IN_PROGRESS | 409 | The first request with this Idempotency-Key is still running | Retry shortly with the same key: you get its answer. |
KEY_ENVIRONMENT | 403 | The key is used outside its environment: a test key (cyp_test_) on a live organization, or a live key (cyp_live_) on a sandbox; or a sandbox's own act asked of a live organization (POST /api/v1/sandbox/reset). Nothing is read or written | Use the organization's own kind of key. See Live and test keys. |
MASS_REMOVAL | 409 | A full-inventory push (POST /api/v1/vehicles/bulk?removeMissing=true) would take more than a fifth of your active vehicles (and more than five) off Cypress, as a truncated feed would. activeInventory and missing say how many. Nothing is written, the items included (since 1.8.0) | Check the feed. When your inventory is larger than one push, name every VIN you hold in inventoryVins on the last page; when the removal is meant, send allowMassRemoval=true. See Pushing inventory. |
Planning codes (1.3.0)
From the Planning API. QUARTER_CLOSED is the one code added since (1.4.0):
| Code | Status | When | What to do |
|---|---|---|---|
PLAN_BUSY | 409 | Another write took the plan's next version at the same moment. Nothing was written. Carries Retry-After: 2. On a planning push the vehicle is still written — the code rides on its 201 (200 on an upsert) | Retry the same request after the header's seconds (a push: bench it with POST /api/v1/planning/moves, or re-push with ?mode=upsert). |
IN_SELL | 409 | A planning-stock-only route — odometer readings, departure — on a vehicle that is in Sell | Use the Sell route: PATCH for mileage, /disposition for a sale elsewhere. |
RETIRED | 409 | A planning-stock-only route — odometer readings, departure — on a vehicle that has already left the fleet (archived) | Nothing — its record is closed. |
ODOMETER_ROLLBACK | 409 | The reading runs the odometer series backwards; details.recorded is the reading it conflicts with | Check the figure. For a replaced or corrected odometer, send force: true. |
NO_PLAN | — | Not a refusal: on a planning push's 201 (200 on an upsert), the vehicle was written in OPERATION but there is no live plan to bench it in | Bench it later with POST /api/v1/planning/moves (to: BENCH). |
PREVIEW_STALE | 409 | A cohort confirm whose preview expired (24 hours) or whose cohort changed since. Since 1.5.0 also a schedule apply whose proposal expired or whose plan changed, and a Sell quarter approval whose digest is not the live plan's | Preview (or read the quarter) again, then send the new digest. |
KIND_CHANGE | 409 | PATCH /api/v1/planning/goals/{id} that sends params of another kind: a goal keeps its kind (units, cash, shape, renewal). Since 1.5.0. | Add a new goal of that kind, and remove the old one if you no longer want it. |
WINDOW_CLOSED | 409 | A move into, or a hand-off (preview or confirm) from, a sale window that has closed in a quarter still in progress. A window of a quarter that is over answers QUARTER_CLOSED instead: the quarter is asked first | Use an open window — GET /api/v1/planning/plan (state, and closed since 1.4.0). |
WINDOW_GONE | 404 | A move named a window the latest plan version no longer has. An id from an earlier version is accepted — it is followed by its lineage — so this means the window itself is gone (or the id is unknown) | Re-read the plan (GET /api/v1/planning/plan) and pick an open window. |
SELLING_BEGUN | 409 | A demotion of a vehicle that is listed or beyond | Delist it first, or ask for an archive review. |
RESOLVED | 409 | Resolving or editing a blocker that is already resolved | Nothing — a resolved blocker is history. |
QUARTER_PASSED | 422 | Parking until a quarter that has already started | Pick a coming quarter. |
QUARTER_CLOSED | 409 | The request would change a quarter that is over — its last day has passed, UTC. A window counts in the quarter that holds its 8th day. Refused: a move into one of its windows; a move to OPERATION, or a park, of a vehicle scheduled in one; a track change of a planned vehicle (not yet handed to Sell) in one; the hand-off preview or confirm of one of its windows. The request is refused whole and nothing was written. details.vehicleIds lists the vehicles it blocks, details.quarter (2026-Q3) their quarter when it is one | Nothing to retry — the quarter does not reopen. Use a window of a quarter in progress (GET /api/v1/planning/plan: closed: false), or send the request again without the vehicles named in details.vehicleIds. See Closed quarters. Since 1.4.0. |
QUARTER_PASSED and QUARTER_CLOSED are different codes: QUARTER_PASSED
(422) is parking until a quarter that has already started;
QUARTER_CLOSED (409) is acting on a quarter that is over.
Buy planning codes (1.4.0)
From the Planning API (Buy planning).
QUARTER_CLOSED is the same code as above, here for the dealer's own plan
(it carries no details), and BUY_PLANNING_NOT_ENABLED is in the table
above. The approval of a quarter has its own three codes, and two more guard
the removal of a target and of a purchase:
| Code | Status | When | What to do |
|---|---|---|---|
QUARTER_CLOSED | 409 | The goal of a quarter that is over (PUT /quarters/{periodKey}), or a target needed in one: adding it, pausing, reopening, closing, editing or removing it, or an edit that would move one into such a quarter. Nothing was written. A purchase is never refused with it — it is a fact | Nothing to retry — the quarter does not reopen. Add the target to a week of a quarter in progress. |
PLAN_CHANGED | 409 | POST /quarters/{periodKey}/approve with a digest that is not the live plan's: the plan moved since you read the quarter. "The plan changed. Refresh and approve again." | Read the quarter again and approve the new digest. |
PERIOD_CLOSED | 409 | POST /quarters/{periodKey}/approve on a quarter that is over, Buy or (since 1.5.0) Sell. (The approval's own code; the goal of such a quarter answers QUARTER_CLOSED.) | Nothing — a quarter that is over cannot be approved. |
NOTHING_TO_APPROVE | 409 | POST /quarters/{periodKey}/approve on a quarter with no goal, no budget and no target: "Set a goal or add a target first." | Set a goal or add a target, then approve. |
HAS_PURCHASES | 409 | DELETE /targets/{id} on a target that has purchases | Close it instead (PATCH with { "status": "CLOSED" }); its history stays. |
FROM_CLOSING | 409 | DELETE /purchases/{id} on a purchase that came from a Cypress closing | Nothing — it follows its closing. Only a purchase you recorded can be removed. |
Another organization's target or purchase is 404, as one that does not exist.
A body that doesn't validate is 422 with the same error and details as
everywhere else.
Subscribing to the BUY.* webhooks (POST or PUT on
/api/v1/webhooks/subscriptions) answers 403 SCOPE_MISSING when the key
lacks buy-planning:read, 403 BUY_PLANNING_NOT_ENABLED when the organization
has no Buy planning, and 422 when the subscription would use the MANHEIM
names ("BUY.* events are available only on the Cypress dialect"). See
Webhooks.
A bulk planning move (moves, moves/track, moves/park, moves/unpark)
doesn't refuse a vehicle it can't move — it lists it in skipped with a
reason, including an unknown id or another organization's (not in the plan), and moves the rest. It answers 409 only when every vehicle was
skipped.
One exception: a closed quarter. A request that would change a vehicle
in a window of a quarter that is over is refused whole with
409 QUARTER_CLOSED — nothing is applied, not even to the vehicles that
could have changed — and details.vehicleIds lists exactly the vehicles it
blocks, so you can send the request again without them:
{
"error": "This quarter is closed. A closed quarter is history, so its plan no longer changes.",
"code": "QUARTER_CLOSED",
"details": { "vehicleIds": ["veh_a…", "veh_c…"], "quarter": "2026-Q3" }
}details.quarter is present when those vehicles' windows all count in the
same quarter. error is one fixed sentence: switch on code.
Echoing lifeStage is safe
lifeStage is read-only. Sending back the value you read — or any value on
the same side of the line, such as POST_SALE for a vehicle in Sell — is
accepted and ignored, so a client that copies lifeStage from a read into
its next PATCH or PUT keeps working.
Changed in 1.4.0
API 1.4.0 added Buy planning over the API (new scopes, routes, codes and webhooks; the
scope strings and everything that already existed are unchanged), and changed
one thing for existing keys that this page is about: a request that changes a
quarter that is over now answers 409 QUARTER_CLOSED. (The other change is a
delivery, not an error: OFFERS.ACCEPTED is now also sent when a buyer accepts a
counter. See Webhooks.) A move into a window of
such a quarter, a move to OPERATION or a park of a vehicle scheduled in one,
a track change of a planned vehicle in one, and the hand-off of one of its
windows used to be accepted, except that a move into a window, or a hand-off,
answered 409 WINDOW_CLOSED when the window itself had closed (which is now
answered only for a window that has closed in a quarter still in progress). A
hand-off preview of such a window answers QUARTER_CLOSED too, instead of a
digest the confirm would refuse. A vehicle
already in Sell can still change track, and blockers and demotions are not
refused: see Closed quarters.
Before 1.1.0
Refusals carried only error (and details on 422). The code field and
PLANNING_STOCK/LIFE_STAGE_READ_ONLY arrived in API 1.1.0;
ORG_NOT_ACTIVE, PLANNING_NOT_ENABLED, SCOPE_MISSING and
CHANNEL_NOT_GRANTED arrived in 1.2.0, alongside GET /api/v1/organization
(see Your organization and key); the
planning codes arrived in 1.3.0 with the Planning API, and QUARTER_CLOSED
in 1.4.0.
Some older statuses changed. API 1.2.0 changed planning stock at an
organization without Planning from 409 PLANNING_STOCK to
403 PLANNING_NOT_ENABLED, and changed a vehicles-only key pushing a
vehicle/unit with an embedded listing/offering from creating it (with
the listing silently dropped) to 403 SCOPE_MISSING. API 1.3.0 changed:
- A
PATCH /api/v1/vehicles/{id}on planning stock, at an organization with Planning, from a key withoutplanning:write, now answers403 SCOPE_MISSING; 1.2.0 answered409 PLANNING_STOCK. (A body naming prices,listingorimagesstill answers409 PLANNING_STOCK; without Planning it is still403 PLANNING_NOT_ENABLED.) - A
POST /api/compatible/v1/units/bulkitem whose embeddedofferingnames an ungranted channel used to create the unit without the offering and answer201; it now answers that item422 CHANNEL_NOT_GRANTEDand creates nothing.
API 1.3.0 also gives existing keys one new capability:
GET/PUT /api/v1/vehicles/{id}/ownership needs only the vehicles
scope for a vehicle in Sell, so a key with the default scopes can read and
replace the purchase & financing record of its organization's Sell
vehicles. And webhooks about planning stock — VEHICLES.* for a vehicle in
Operation or Planning, and every PLAN.* event — reach only a
Cypress-dialect subscription that lists them, so an unfiltered or MANHEIM
subscription receives none of them (see
Webhooks). Every other
older status and message is unchanged.