cypress.DOCS
Log inRegister
Cypress Docs
Developers

Planning API (Sell planning)

Push your fleet into Sell planning, keep it current, read the plan, drive it and plan the quarter — the same rules the app uses, since API 1.3.0. Closed quarters since 1.4.0; the strategy, goals, the quarter's approval and Sequoia's schedule since 1.5.0.

TL;DRAI summary

This chapter explains how to use the Cypress Planning API to manage vehicles before they are sold. You can push vehicles into the system, update their details, and move them through different planning stages until they are ready to be handed off to Sell. This ensures your fleet always has a clear plan for vehicle remarketing.

Dive deeper:

With Planning switched on for your organization, the API reaches the step before Sell: which vehicles leave the fleet, in which sale window, and when they are handed to Sell. Your fleet system pushes vehicles in, keeps their mileage, costs and financing current, reads the plan, and moves vehicles through it. Every write goes through the same rule the Planning page uses — the API and the app can never disagree.

Nothing here lists, prices, discloses or bills anything. The one exit from Planning is the hand-off (cohort confirm), which puts vehicles in the Sell intake as drafts.

"Planning" on this page means Sell planning, the plan that hands vehicles to Sell. Buy planning, the dealer's plan, has its own API, scopes and product switch: Planning API (Buy planning). The scopes (planning:read, planning:write) and products.planning in Your organization and key mean the Sell side.

Where a vehicle lives

lifeStage OPERATIONlifeStage PLANNINGthe Sell intakeOPERATIONin servicePLANNINGcandidate · scheduledparkedREMARKETINGdraft, then listedbenchto: OPERATIONcohort-confirmdemotionsdeparture → leaves the fleet at once (200)
lifeStageMeansMembership states you will read
OPERATIONIn service — in the plan, not under considerationIN_OPERATION
PLANNINGOn the plan's tableCANDIDATE (the bench), STAGED / CONDITIONAL (in a window; conditional = waiting on a payoff or arrivals blocker), PARKED, SLIPPED (its window closed)
REMARKETINGHanded to SellPROMOTED

Scopes

Every route needs an organization with Planning switched on — without it, each one answers 403 PLANNING_NOT_ENABLED, reads included, and nothing is deleted. Then:

RoutesScopes
/api/v1/planning/* reads (plan, vehicles, blockers GET)planning:read
/api/v1/planning/* writesplanning:write
Push (POST /api/v1/vehicles, /bulk with lifeStage OPERATION/PLANNING), PATCH /api/v1/vehicles/{id} on planning stockvehicles + planning:write
/api/v1/vehicles/{id}/ownership, odometer-readings, cost-events (GET)vehicles + planning:read
The same, written; departurevehicles + planning:write

planning:write implies planning:read. Ownership on Sell stock needs only vehicles; cost events need the planning scopes on any vehicle. See Your organization and key.

Since 1.5.0 the /api/v1/planning/* routes also take an app's or an agent's token (Connect Cypress) with the same scopes. A write is credited to the person the app acts for, or to the agent, and the audit names it. Approving a quarter is a person's: an agent's token that approves answers 403 PEOPLE_ONLY and nothing is written. Everything else on this page is open to an agent.

1. Push the fleet in

POST /api/v1/vehicles (and /bulk, up to 500, and ?mode=upsert by VIN) accepts lifeStage: OPERATION (in service) or PLANNING (benched as a candidate), with an optional ownership block:

{
  "vin": "1FTBR1C82MKA41234",
  "modelYear": 2021, "make": "Ford", "model": "Transit", "trim": "T-250",
  "mileage": 41200,
  "condition": 3.5,
  "location": { "city": "Orlando", "state": "FL", "zip": "32801" },
  "lifeStage": "PLANNING",
  "ownership": {
    "purchasePrice": 38900, "purchaseDate": "2021-03-15",
    "acquisitionMethod": "FINANCING", "loanAmount": 31000,
    "loanTermMonths": 60, "interestRate": 6.9
  }
}
{
  "vehicle": { "id": "clx…", "vin": "1FTBR1C82MKA41234", "status": "DRAFT", "lifeStage": "PLANNING", "mileage": 41200 }
}
  • The vehicle lands as planning stock, in the plan straight away (the base plan opens if your organization has none yet). The mileage is recorded as an odometer reading.
  • Every PLANNING item of one request is benched in one plan version — a bulk of 300 is one version, not 300.
  • No valuation is bought at push. The daily valuation sweep values new planning stock, up to 250 vehicles a run, oldest first — a fleet of about 1,000 is valued within about four days. After that, planning stock refreshes every 90 days. market reads null until then.
  • The ownership block is checked before anything is created: an invalid one answers 422 (bulk: that item) with details.errors, and no vehicle is created. Softer findings come back as warnings and are saved. The block applies only to OPERATION/PLANNING pushes — on a Sell push the key is carried as-is, as in 1.2.0; set a Sell vehicle's ownership with PUT /api/v1/vehicles/{id}/ownership.
  • An embedded listing on a planning push answers 422 — a listing needs Sell stock.
  • Planning pushes follow this API's field rules (the ones in the API reference), not the app's details form.

Upserts by VIN. An upsert never moves a vehicle across the Planning/Sell line: a VIN in Sell pushed with OPERATION or PLANNING answers 409 LIFE_STAGE_READ_ONLY. Inside Planning, an upsert with PLANNING benches a vehicle that is in OPERATION; an upsert with OPERATION does not take a vehicle off the bench — it answers with a warning pointing at POST /api/v1/planning/moves (to: OPERATION).

When the bench step doesn't land

The vehicle is written before it is benched. If the bench step does not land — the plan was busy, there is no plan, the vehicle was skipped, or the step failed — the push still answers 201 (200 on an upsert; bulk: that item's 201/200) with the vehicle in OPERATION, a planningStep.error saying why, and code PLAN_BUSY (with a Retry-After header) or NO_PLAN when one applies. Finish it with POST /api/v1/planning/moves (to: BENCH), or re-push with ?mode=upsert. Never re-push without upsert — that is a 409 duplicate VIN.

2. Keep it current

Details. PATCH /api/v1/vehicles/{id} on planning stock updates its details through the app's planning details save — every field audited, a mileage change recorded as an odometer reading, no valuation bought. It refuses what belongs to Sell: prices, listing or images answer 409 PLANNING_STOCK; ownership answers 422 (use the ownership route); a different lifeStage answers 409 LIFE_STAGE_READ_ONLY (moves go through the plan). Echoing the current lifeStage is fine. PUT on planning stock stays 409 PLANNING_STOCK — a full replace has no planning meaning.

Purchase & financing. GET /api/v1/vehicles/{id}/ownership returns { ownership, effective } — the record as stored, and what Cypress works out from it (principal, payment, payoff date, balance owed, each tagged ENTERED or CALCULATED). No market value is returned. PUT is a full replace: a field you leave out is cleared. Cross-field errors answer 422 with details.errors; warnings come back on success.

Odometer readings (planning stock only — a vehicle in Sell answers 409 IN_SELL, so send its mileage with PATCH; one that has left the fleet answers 409 RETIRED):

curl -X POST "$BASE/api/v1/vehicles/$VEHICLE_ID/odometer-readings" \
  -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "readAt": "2026-09-30", "miles": 43870 }'
  • miles or km (converted), exactly one; readAt is YYYY-MM-DD, not in the future.
  • A reading dated on or after the latest one becomes the vehicle's mileage. When the vehicle has no reading yet, its current mileage counts as a reading dated today.
  • A reading that runs the series backwards — lower than the latest one on or before its date, or higher than the next one after it — answers 409 ODOMETER_ROLLBACK with details.recorded. Send force: true for a replaced or corrected odometer.
  • A back-dated reading is stored as history and never changes the current mileage, even with force.
  • GET lists the readings newest first, up to 100.

Cost events — maintenance, repairs, fuel, insurance, registration — on any vehicle of your organization, Sell stock included:

{ "category": "REPAIR", "amount": 1240.5, "eventDate": "2026-09-12", "note": "Rear brakes", "externalRef": "WO-88412" }

Send externalRef (your id for the cost) and a retry is safe: a POST whose externalRef is already stored for the vehicle answers 200 with the stored row, unchanged — even if the new body differs. A new one answers 201. GET lists them newest first, up to 100.

Cost events are recorded, not yet read

The plan's carry cost does not read cost events yet — it uses the cost assumptions set in the app (Planning → Costs). Cost events are kept as the vehicle's history, ready for when it does.

Departure (planning stock only; a vehicle in Sell keeps POST /api/v1/vehicles/{id}/disposition and answers 409 IN_SELL here, and one that has already left the fleet answers 409 RETIRED):

{ "kind": "SOLD_OFF_PLATFORM", "reason": "Sold to the lessee at term", "soldPrice": 18500, "soldAt": "2026-09-28" }

Since 1.8.0 a departure is applied at once: planning stock was never on the market, so there is nothing for Cypress to weigh. It answers 200 with archived: true, and your endpoint hears VEHICLES.DISPOSITION.RESOLVED with automatic: true. (Before 1.8.0 a Cypress Manager reviewed every departure; a request filed then still waits, and a second departure answers it with 202 and alreadyPending: true.) LEFT_FLEET refuses soldPrice/soldAt (422); for SOLD_OFF_PLATFORM the sale figures travel in the request's note.

3. Read the plan

RouteReturns
GET /api/v1/planning/planThe strategy, the plan's version, the six sale windows (id, lineage, label, dates, state, since 1.4.0 quarter and closed, and since 1.5.0 links.app, which opens the live window of that lineage) and the tracks (key, name, posture, isDefault). Opens the base plan on the first read if none exists; 404 when the organization has no plannable vehicles yet.
GET /api/v1/planning/vehiclesOne row per vehicle in the latest plan, by rank, paginated (limit, offset), filtered by windowId, windowLineage, state, trackKey, lifeStage. Includes Sell stock adopted by the plan.
GET /api/v1/planning/vehicles/{id}One vehicle's plan view, with the app's verdict and suggestion.

What the list and the detail carry differs on purpose:

FieldList rowDetail
market.value, market.bandthe latest valuationthe app's market position
advice (SELL, HOLD or NEEDS_ASSESSMENT, with reasoning)the latest stored plan advicethe same
market.expectedRealized, realizationBasis, trendPctPerMonth—yes
verdict (since 1.10.0 with its reason), suggestion, plan—yes
economics (since 1.10.0)—yes

The detail is the app's own recommendation, computed per organization and cached — it can be up to 60 seconds stale after a write. The list reads stored values and is always current.

What holding a car costs (since 1.10.0). The detail's economics is what the verdict stands on: points, every other week, the projected value (gross) and what you end up with if it is released that week (net, after the selling cost and what it earned or cost while held); bestWindow, the weeks it sells best, the best one (bestWeek) and whether now is in them; waits, the value's change against today after 1, 3 and 6 months; and monthlyCost, what one more month costs in value (valueLoss) and to run (running), from weeklyRunningCost. basis says what the window stands on: CONTRIBUTION, your operating data models the hold, or DECAY_ONLY, nothing it earns is modeled, so earlier is always better. economics is null when the vehicle has no value.

reasoning is free text for people. Don't parse it. (So are plan.note and verdict.reason.) Neither carries a plan version number any more — the Planning page no longer shows one — so text stored before 1.4.0 ("Sequoia plan v12: …") reads "Sequoia's live plan: …" here.

  • Window ids change with every plan version. A write accepts an id you read from an earlier version — a move or a cohort call follows it by its lineage (2026W44-2026W45) to the latest version's window, so a retry after PLAN_BUSY, or a second move after any write, needs no re-read. Follow a window yourself by lineage: ?windowLineage= filters by it.
  • quarter and closed tell you which windows can still be written (since 1.4.0). A window counts in the quarter that holds its 8th day, quarter names it (2026-Q4), and closed is true once that quarter is over — its last day has passed, UTC. It is the test the server refuses by, so pick windows with closed: false; do not read state, because a window of a closed quarter that has not been swept yet still reads PLANNED or ACTIVE. A vehicle's window carries the same two fields. See Closed quarters.
  • A row's track is the effective track — the default track for a vehicle not on one — and ?trackKey=<the default track's key> includes those vehicles.
  • A read may enroll vehicles new to the plan, exactly as opening the Planning page does.

4. Drive the plan

Every write is one plan version, with your reason in its note and API · <key name> in the audit. Two writes racing for the same version: the loser answers 409 PLAN_BUSY with Retry-After: 2 and writes nothing — wait and retry the same request. Two writes are not idempotent across a network timeout: a retried blocker add records a second blocker, and a retried cohort confirm answers 409 PREVIEW_STALE after the first one succeeded — read the plan (GET /api/v1/planning/blockers?vehicleId=, GET /api/v1/planning/vehicles) to see whether it landed before retrying.

planVersion, in the answer of a write, and plan.version on a read count revisions of your plan: use them to correlate your writes with the audit and your reads. It is not a version you choose or send back, and the Planning page no longer shows version numbers.

RouteBodyDoes
POST /api/v1/planning/movesvehicleIds (1–50), to: { "windowId": … }, "BENCH" or "OPERATION", reason?Schedules into a window, benches as candidates, or takes vehicles back to operation. One target per call. Into a window of a closed quarter: 409 QUARTER_CLOSED.
POST /api/v1/planning/moves/trackvehicleIds, trackKey, reason?Puts vehicles on a track. A vehicle in Sell keeps selling; its track label changes, also after its window's quarter is over. A planned vehicle in a closed quarter's window: 409 QUARTER_CLOSED.
POST /api/v1/planning/moves/parkvehicleIds, untilQuarter (2027-Q1), reason?Parks until a coming quarter (422 QUARTER_PASSED for one already started); the daily sweep brings them back. A vehicle scheduled in a closed quarter's window: 409 QUARTER_CLOSED.
POST /api/v1/planning/moves/unparkvehicleIdsBack to the bench.
GET /api/v1/planning/blockers?vehicleId=—The vehicle's blockers, open first.
POST /api/v1/planning/blockersvehicleId, kind (PAYOFF, EVIDENCE, ARRIVALS, AUTHORITY, OTHER), noteAdds a blocker (201).
PATCH /api/v1/planning/blockers/{id}{ "resolvingRef": … } (5+ characters) — or kind / noteResolves it with the evidence that cleared it, or edits it. A resolved blocker answers 409 RESOLVED.
POST /api/v1/planning/demotionsvehicleId, reason?Brings one vehicle back from the Sell intake to the bench — only before selling begins (409 SELLING_BEGUN once listed).

Bulk moves skip, single ids 404. A move, track, park or unpark names what it could not do in skipped — each vehicleId with its reason — and applies the rest. An unknown id, or another organization's, is skipped as not in the plan; it never touches anyone else's plan. If every vehicle is skipped the call answers 409 with skipped. Routes that take one id — a vehicle, a blocker, a window — answer 404 for an id that isn't yours. (The one request that is refused whole instead is a change to a closed quarter: Closed quarters.)

to: "BENCH" and to: "OPERATION" skip a vehicle that is in Sell (in Sell — remarketing owns it); to: "BENCH" also skips one that is already a candidate (already a candidate) or already scheduled in a window (already scheduled); to: "OPERATION" skips one already there (already in operation). The API benches a vehicle from operation, from parking or from a window that closed on it, but it cannot take a scheduled vehicle back to the bench: the app's Back to the bench for a scheduled vehicle has no call on the API.

A blocker gates the sale, not the planning: an EVIDENCE blocker stops a vehicle being scheduled; a PAYOFF or ARRIVALS blocker schedules it as waiting (CONDITIONAL). Resolving the last open blocker of a waiting vehicle makes it STAGED in the same version. Deleting a blocker is not on the API.

The hand-off — preview, then confirm

Handing a window to Sell takes two calls, so you confirm exactly the cohort you saw.

  1. Preview — writes nothing:

    curl -X POST "$BASE/api/v1/planning/windows/$WINDOW_ID/cohort-preview" -H "x-api-key: $KEY"
    {
      "windowId": "win_…", "planVersion": 42,
      "digest": "c4a1f09e…", "expiresAt": "2026-10-02T09:14:00.000Z",
      "declaredEffects": ["INTERNAL_INTAKE"],
      "doesNot": ["list", "disclose to buyers", "accept offers", "release operating capacity", "bill anything"],
      "members": [
        { "vehicleId": "veh_a…", "vin": "1FTBR1C82MKA41234", "label": "2021 Ford Transit", "state": "STAGED", "disposition": "PROMOTE", "blockers": [] },
        { "vehicleId": "veh_b…", "vin": "1FTBR1C89MKA41299", "label": "2021 Ford Transit", "state": "CONDITIONAL", "disposition": "HOLD_CONDITIONAL", "blockers": ["PAYOFF: Lien release pending"] }
      ]
    }
  2. Confirm — echo both digest and expiresAt:

    curl -X POST "$BASE/api/v1/planning/windows/$WINDOW_ID/cohort-confirm" \
      -H "x-api-key: $KEY" -H "content-type: application/json" \
      -d '{ "digest": "c4a1f09e…", "expiresAt": "2026-10-02T09:14:00.000Z" }'
    {
      "planVersion": 43,
      "results": [
        { "vehicleId": "veh_a…", "result": "INTAKE_CONFIRMED" },
        { "vehicleId": "veh_b…", "result": "HELD_BLOCKER_OPEN" }
      ]
    }

INTAKE_CONFIRMED vehicles are in the Sell intake (lifeStage REMARKETING, status still DRAFT — nothing is listed); they now show in GET /api/v1/vehicles. HELD_BLOCKER_OPEN vehicles wait on an open blocker; BLOCKED_EVIDENCE ones are untouched. A preview is good for 24 hours: an expired one, or a cohort that changed since (a vehicle moved, a blocker added), answers 409 PREVIEW_STALE — preview again. A window that has closed, in a quarter still in progress, answers 409 WINDOW_CLOSED; a window of a quarter that is over answers 409 QUARTER_CLOSED, preview included — see below.

Closed quarters

A quarter whose last day has passed is history, and since 1.4.0 the plan of a closed quarter no longer changes, over the API as in the app. A quarter is over from the first instant after its last day, UTC. A sale window counts in the quarter that holds its 8th day, so a window that starts on September 21 belongs to Q3 and closes with it, although it ends in October. GET /api/v1/planning/plan says it for you: each window carries quarter and closed.

What answers 409 QUARTER_CLOSED:

CallRefused when
POST /planning/moves, to: { "windowId": … }the window is in a closed quarter
POST /planning/moves, to: "OPERATION"a vehicle scheduled in a window of a closed quarter would be taken out of it
POST /planning/moves/tracka planned vehicle (not yet handed to Sell) sits in a window of a closed quarter
POST /planning/moves/parka scheduled vehicle sits in a window of a closed quarter
POST /planning/windows/{windowId}/cohort-preview and /cohort-confirmthe window is in a closed quarter

to: "BENCH" and unpark never meet it: the API does not bench a scheduled vehicle, and a parked vehicle has no window.

{
  "error": "This quarter is closed. A closed quarter is history, so its plan no longer changes.",
  "code": "QUARTER_CLOSED",
  "details": { "vehicleIds": ["veh_a…"], "quarter": "2026-Q3" }
}
  • Nothing was written, and it is not retryable. The request is refused whole, even when it also named vehicles that could have changed; details.vehicleIds lists exactly the vehicles the closed quarter blocks, and details.quarter their quarter when they share one. Send the rest again without them, or pick a window with closed: false. There is no Retry-After: waiting does not reopen a quarter.
  • It is asked before the window's own state, so a window of a closed quarter answers QUARTER_CLOSED, not WINDOW_CLOSED. (QUARTER_PASSED is a different code: parking until a quarter that has started.)
  • It depends on the plan and the date, not on your key, so GET /api/v1/organization cannot predict it.

What stays possible on a closed quarter:

  • A vehicle already in Sell (handed to Sell) can still change track: a car that is still selling has a pricing posture that must not freeze. Only planned vehicles, not yet handed over, are frozen with their quarter.
  • A vehicle that slipped (its window closed unsold) can leave its window, and an unsold vehicle can be moved from a closed window into a window of a quarter in progress.
  • Blockers can still be added, edited and resolved, and a vehicle handed to Sell can still be pulled back to planning with a demotion (it goes to today's bench). They are facts, or the vehicle's future, like recording a purchase made elsewhere — not a change to the quarter's plan.
  • Reads, odometer readings, cost events and ownership are not plan changes.

5. Webhooks

EventWhen
PLAN.VEHICLE.ENTEREDBenched as a candidate (a PLANNING push, a move to BENCH, the app)
PLAN.VEHICLE.SCHEDULEDScheduled into a sale window
PLAN.WINDOW.DUEIts window's hand-to-Sell date arrived
PLAN.WINDOW.MISSEDIts window closed without it
PLAN.BLOCKER.ADDED / PLAN.BLOCKER.RESOLVEDA blocker added, or resolved (removing one frees the vehicle too, and is told the same way)
PLAN.HANDED_TO_SELLHanded to Sell by a cohort confirm
PLAN.GOAL.SETA goal of the quarter added or changed (since 1.5.0; the body is the goal, not a vehicle)
PLAN.QUARTER.APPROVEDA person approved the quarter's plan (since 1.5.0; the body is the approval)

PLAN.* events are opt-in: they go only to a subscription that lists them in eventTypes — never to an unfiltered "all events" subscription — and only in the Cypress dialect (a MANHEIM-dialect subscription that lists one is refused with 422). Subscribing needs planning:read, and no new PLAN.* event is sent once Planning is switched off (deliveries already under way still retry). A vehicle pushed as OPERATION sends no PLAN.VEHICLE.ENTERED — it is in service, not under consideration.

Vehicle events about planning stock follow the same rules. VEHICLES.CREATED, VEHICLES.UPDATED and VEHICLES.DISPOSITION.STAGED / RESOLVED for a vehicle in Operation or Planning — a planning push, a PATCH, a departure — reach only a Cypress-dialect subscription that lists the event, while the organization has Planning; an unfiltered or MANHEIM subscription never sees planning stock. When a cohort confirm hands vehicles to Sell, each one also sends a plain VEHICLES.UPDATED, delivered like any Sell event — that is how a Sell-side integration learns the vehicle is its own now.

{
  "href": "https://app.getcypress.ai/api/v1/planning/vehicles/veh_a…",
  "eventType": "PLAN.VEHICLE.SCHEDULED",
  "resource": "plan",
  "body": {
    "vehicleId": "veh_a…", "vin": "1FTBR1C82MKA41234",
    "membershipState": "STAGED",
    "window": { "id": "win_…", "lineage": "2026W44-2026W45", "startDate": "2026-10-26T00:00:00.000Z", "endDate": "2026-11-08T00:00:00.000Z" },
    "track": { "key": "sell-fast", "name": "Sell fast" },
    "plannedValue": 24800,
    "openBlockers": 0
  },
  "createdOn": "2026-10-01T09:14:00.000Z",
  "relatedResources": [{ "href": "https://app.getcypress.ai/api/v1/vehicles/veh_a…" }]
}

openBlockers is a count — read them with GET /api/v1/planning/blockers. track is the effective track — the default track for a vehicle not on one, as GET /api/v1/planning/vehicles reports it. See Webhooks for signatures and retries.

6. Plan the quarter

Since 1.5.0 the quarter itself is on the API: the mandate (the strategy), the quarter's goals, Sequoia's schedule, and the quarter's approval. These are the calls the Planning page's Plan my quarter with Sequoia makes. Nothing here lists, prices or hands anything to Sell: a plan is a proposal until a person approves the quarter, and the hand-off stays the cohort confirm.

RouteScopeDoes
GET /api/v1/planning/strategyreadThe active strategy (name, objective, constraints), or null for the base plan.
POST /api/v1/planning/strategywriteSets the mandate: retires the active strategy (its history stays), makes plan v1 (six two-week sale windows from today) and enrolls the whole fleet. With goals, adds the quarter's goals in the same call, each its own plan version. 201.
DELETE /api/v1/planning/strategywriteEnds the strategy. The base plan reopens on the next read.
GET /api/v1/planning/goals?periodId=readThe quarter's goals with their monthly pacing.
POST /api/v1/planning/goalswriteAdds a goal. 201.
PATCH /api/v1/planning/goals/{id}writeChanges a goal's number, date, label, pacing or track. A goal keeps its kind (409 KIND_CHANGE).
DELETE /api/v1/planning/goals/{id}writeRemoves a goal.
GET /api/v1/planning/quarters/{periodKey}readThe quarter: goals and their attainment, lots per month, windows with their hand-to-Sell dates, the fleet's shape, the sell-through ladder, and approvalDigest. periodKey is 2026-Q4 or current.
POST /api/v1/planning/quarters/{periodKey}/approvewriteBody { "digest" }, the quarter's approvalDigest. The approved plan becomes the baseline. A person's.
POST /api/v1/planning/schedule/proposalswriteSequoia's schedule for a quarter (periodId or periodKey, or neither for the current one): which vehicles into which windows, why, what could not be placed. Writes nothing.
POST /api/v1/planning/schedule/applywriteSend back the proposal's periodId, digest and expiresAt (and vehicleIds for some lines only). One plan version.

A goal is one of four kinds, in params:

{ "kind": "UNITS", "units": 12 }
{ "kind": "UNITS", "units": 4, "lotKey": "2021|NISSAN|SENTRA" }
{ "kind": "CASH", "amount": 250000, "byDate": "2026-12-31" }
{ "kind": "SHAPE", "avgAgeMaxYears": 4, "mileageMax": 80000 }

RENEWAL (releases ahead of the arrivals you expect each month) is the fourth; see the reference. Left out, monthlyTargets splits the goal over the quarter's months, as the app does.

The sell draft in one call. An agent drafts a quarter with one POST /api/v1/planning/strategy that carries goals, then asks Sequoia for the schedule and applies it. A person reads the quarter in the app (or through an app acting for them) and approves it. A goal that is refused does not undo the strategy: the answer's goals lists each goal as written, or its error and code.

Digests bind the decision to what was read. The approval is refused with 409 PREVIEW_STALE when the plan changed since the quarter was read, and so is a schedule apply when the plan changed since the proposal or the proposal is more than 24 hours old. Read again and send the new digest. A quarter that is over answers 409 QUARTER_CLOSED to a goal or a schedule, and 409 PERIOD_CLOSED to an approval. Every Planning write (setting and ending the strategy, adding, changing and removing a goal, approving a quarter, applying the schedule, and the older ones: blockers, moves to a window, the bench or operation, parking, tracks, demotions and handing a window to Sell) takes ?dryRun=true, which meets the same checks and refusals and answers { "dryRun": true, "changes": … } with no plan version made, and an Idempotency-Key, which makes a retry answer the same, once. Since 1.5.0.

What stays in the app

Lot targets, Sequoia's decisions and its market rail, tracks and groups, cost assumptions, and deleting a blocker are not on the API. The Market board, the forecast and the Cypress desk are the app's as well. Buy planning (the dealer's plan) has its own API: Planning API (Buy planning). How the plan works is in the Planning manual: Sell planning, starting with the plan, and Buy planning.

Try it

The integration kit's 5-planning folder walks the whole path in twelve requests — push, read, move into a window, a payoff blocker added and resolved, the hand-off, an odometer reading, a cost and a departure — with ids and the preview's digest captured for you. See the Quickstart.