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.
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 | Means | Membership states you will read |
|---|---|---|
OPERATION | In service — in the plan, not under consideration | IN_OPERATION |
PLANNING | On the plan's table | CANDIDATE (the bench), STAGED / CONDITIONAL (in a window; conditional = waiting on a payoff or arrivals blocker), PARKED, SLIPPED (its window closed) |
REMARKETING | Handed to Sell | PROMOTED |
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:
| Routes | Scopes |
|---|---|
/api/v1/planning/* reads (plan, vehicles, blockers GET) | planning:read |
/api/v1/planning/* writes | planning:write |
Push (POST /api/v1/vehicles, /bulk with lifeStage OPERATION/PLANNING), PATCH /api/v1/vehicles/{id} on planning stock | vehicles + planning:write |
/api/v1/vehicles/{id}/ownership, odometer-readings, cost-events (GET) | vehicles + planning:read |
The same, written; departure | vehicles + 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
PLANNINGitem 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.
marketreadsnulluntil then. - The
ownershipblock is checked before anything is created: an invalid one answers422(bulk: that item) withdetails.errors, and no vehicle is created. Softer findings come back aswarningsand are saved. The block applies only toOPERATION/PLANNINGpushes — on a Sell push the key is carried as-is, as in 1.2.0; set a Sell vehicle's ownership withPUT /api/v1/vehicles/{id}/ownership. - An embedded
listingon a planning push answers422— 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 }'milesorkm(converted), exactly one;readAtisYYYY-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_ROLLBACKwithdetails.recorded. Sendforce: truefor a replaced or corrected odometer. - A back-dated reading is stored as history and never changes the
current mileage, even with
force. GETlists 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
| Route | Returns |
|---|---|
GET /api/v1/planning/plan | The 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/vehicles | One 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:
| Field | List row | Detail |
|---|---|---|
market.value, market.band | the latest valuation | the app's market position |
advice (SELL, HOLD or NEEDS_ASSESSMENT, with reasoning) | the latest stored plan advice | the 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
idyou read from an earlier version — a move or a cohort call follows it by itslineage(2026W44-2026W45) to the latest version's window, so a retry afterPLAN_BUSY, or a second move after any write, needs no re-read. Follow a window yourself bylineage:?windowLineage=filters by it. quarterandclosedtell you which windows can still be written (since 1.4.0). A window counts in the quarter that holds its 8th day,quarternames it (2026-Q4), andclosedistrueonce that quarter is over — its last day has passed, UTC. It is the test the server refuses by, so pick windows withclosed: false; do not readstate, because a window of a closed quarter that has not been swept yet still readsPLANNEDorACTIVE. A vehicle'swindowcarries the same two fields. See Closed quarters.- A row's
trackis 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.
| Route | Body | Does |
|---|---|---|
POST /api/v1/planning/moves | vehicleIds (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/track | vehicleIds, 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/park | vehicleIds, 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/unpark | vehicleIds | Back to the bench. |
GET /api/v1/planning/blockers?vehicleId= | — | The vehicle's blockers, open first. |
POST /api/v1/planning/blockers | vehicleId, kind (PAYOFF, EVIDENCE, ARRIVALS, AUTHORITY, OTHER), note | Adds a blocker (201). |
PATCH /api/v1/planning/blockers/{id} | { "resolvingRef": … } (5+ characters) — or kind / note | Resolves it with the evidence that cleared it, or edits it. A resolved blocker answers 409 RESOLVED. |
POST /api/v1/planning/demotions | vehicleId, 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.
-
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"] } ] } -
Confirm — echo both
digestandexpiresAt: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:
| Call | Refused 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/track | a planned vehicle (not yet handed to Sell) sits in a window of a closed quarter |
POST /planning/moves/park | a scheduled vehicle sits in a window of a closed quarter |
POST /planning/windows/{windowId}/cohort-preview and /cohort-confirm | the 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.vehicleIdslists exactly the vehicles the closed quarter blocks, anddetails.quartertheir quarter when they share one. Send the rest again without them, or pick a window withclosed: false. There is noRetry-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, notWINDOW_CLOSED. (QUARTER_PASSEDis 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/organizationcannot 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
| Event | When |
|---|---|
PLAN.VEHICLE.ENTERED | Benched as a candidate (a PLANNING push, a move to BENCH, the app) |
PLAN.VEHICLE.SCHEDULED | Scheduled into a sale window |
PLAN.WINDOW.DUE | Its window's hand-to-Sell date arrived |
PLAN.WINDOW.MISSED | Its window closed without it |
PLAN.BLOCKER.ADDED / PLAN.BLOCKER.RESOLVED | A blocker added, or resolved (removing one frees the vehicle too, and is told the same way) |
PLAN.HANDED_TO_SELL | Handed to Sell by a cohort confirm |
PLAN.GOAL.SET | A goal of the quarter added or changed (since 1.5.0; the body is the goal, not a vehicle) |
PLAN.QUARTER.APPROVED | A 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.
| Route | Scope | Does |
|---|---|---|
GET /api/v1/planning/strategy | read | The active strategy (name, objective, constraints), or null for the base plan. |
POST /api/v1/planning/strategy | write | Sets 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/strategy | write | Ends the strategy. The base plan reopens on the next read. |
GET /api/v1/planning/goals?periodId= | read | The quarter's goals with their monthly pacing. |
POST /api/v1/planning/goals | write | Adds a goal. 201. |
PATCH /api/v1/planning/goals/{id} | write | Changes a goal's number, date, label, pacing or track. A goal keeps its kind (409 KIND_CHANGE). |
DELETE /api/v1/planning/goals/{id} | write | Removes a goal. |
GET /api/v1/planning/quarters/{periodKey} | read | The 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}/approve | write | Body { "digest" }, the quarter's approvalDigest. The approved plan becomes the baseline. A person's. |
POST /api/v1/planning/schedule/proposals | write | Sequoia'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/apply | write | Send 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.
Vehicle lifecycle
What happens to a vehicle from the moment you push it to the moment it sells.
Planning API (Buy planning)
Keep a dealer's buy plan in step from your purchasing system — the quarter, targets, live matches and purchases, the same rules the app uses, and webhooks that tell you when a match appears, a target fills or a quarter ends short, since API 1.4.0.