Open Application Platform Docs

Editing plans

Editing plans

The plan calls price and judge a plan that you hold. The plan is never stored: each call carries the whole plan and answers the plan it produced. This page describes the edit calls and the distance snapshot that makes repeated edits of one plan fast.

The plan

Every plan call takes the same plan as plan re-costing (POST /v1/plan/recost):

  • vehicles, each with an id and a start, and optionally an end, return_to_start, a capacity, skills and a working window;
  • jobs, each with an id and a location, and optionally a service time, an amount, skills and time windows;
  • routes, each a vehicle_id and its stops as ordered job ids. A job on no route is unassigned.

The edit calls require an id on every vehicle and job, because an edit names them.

The edit calls

Call Capability What it does
POST /v1/plan/recost plan.recost Prices the plan exactly as given.
POST /v1/plan/validate plan.validate Judges the plan exactly as given, without the map payload.
POST /v1/plan/resequence plan.resequence Gives the route of vehicle_id the stop order in stops.
POST /v1/plan/move plan.move Moves the jobs in stops to the route of vehicle_id at position.
POST /v1/plan/remove plan.remove Takes the jobs in stops off their routes.

An edit call applies its edit and prices the edited plan exactly as plan re-costing prices it: the routes and summary of the answer are those a re-costing of the edited plan returns. The answer adds plan, the edited plan in the shape it is sent (routes with ordered stop ids, and unassigned: every job on no route), so it can replace the plan you hold.

  • Positions count from 1, in driving order. A route sent with reverse is read reversed, and comes back in plan already reversed.
  • plan.move takes the stops out first: position counts the stops of the target route without them, and an omitted position places them after the last stop. A stop may come from any route or be a job on no route. move.feasible is true when the routes the move touched keep every stated rule.
  • plan.remove refuses a job that is on no route. A route left without stops is priced from its start to its end.

Violations are reported on the stops and routes, never refused. Every call is billed per node: the stops of the edited plan that were priced.

The search calls

Call Capability What it does
POST /v1/plan/insert plan.insert Places the new jobs in insert at their cheapest positions.
POST /v1/plan/refine plan.refine Re-orders the stops of chosen routes around frozen stops.
POST /v1/plan/what-if plan.what_if Prices and ranks alternative plans.

Every distance and time in their answers is measured on the road, as plan re-costing measures it. With a distance snapshot, the candidates are ranked on the snapshot's road matrix, which is faster. The positions and orders found can then differ from those found without it, but every figure answered is measured the same way.

Inserting jobs

  • The jobs in insert (up to 20) are placed one at a time, in the order given. Each goes to the position that keeps the plan within its rules and adds the least driver time (objective duration, driving plus waiting) or distance (distance). Nothing else in the plan moves.
  • insert in the answer lists, per job, whether it was placed and up to limit choices (default 3), best first, each with its route, position, added distance and time, and arrival. A job that no position can take stays in unassigned.
  • Without a snapshot, the positions beside the nearest stops (default 25) and every empty route are measured.
  • Billed per node: the stops of the plan plus the new jobs.

Refining routes

  • The stops of the routes of vehicle_ids (default: every route) are re-ordered within and between those routes. Stops in freeze stay on their route in their order. Unassigned jobs and other routes do not take part.
  • The new order is kept only when it is better than the plan as sent: fewer unreachable legs, then fewer violations, then less driver time, then less distance. refine.changed says whether it was kept, and refine.before gives the totals of the plan as sent.
  • With a snapshot, up to 600 stops are refined. Without a usable snapshot, up to 60 stops on 5 routes are refined by the routing engine. A larger refine answers PLAN_REFINE_REFUSED (422) with the reason in reason: no_snapshot, too_large, stops_left_out or failed. Refine such a plan with POST /v1/routing/refine, which runs as a job.
  • Single jobs only: a request with pickup and delivery pairs is refused.
  • Billed per node: the stops priced.

Comparing plans

  • plans holds 2 to 10 alternatives over the vehicles and jobs of the request, each with its routes and an optional id. Each plan is priced and judged as plan re-costing does.
  • The plans are ranked by fewer unreachable legs, then fewer violations, then the objective. The answer gives ranking (plan indices, best first), best, and each plan with its rank, summary and routes. Leg geometry is included only with options.geometry.
  • With rank_only and a snapshot, the plans are ranked by their travel on the snapshot's road matrix and are not priced (scored is false). Time windows, capacity and the other rules are not part of that ranking.
  • Billed per node: the stops of every plan.

Distance snapshots

Each call measures every leg of the plan on the road network. While one plan is edited, most legs stay the same from one call to the next. A distance snapshot keeps the legs that were measured, so a later call measures only the legs it has not seen.

  1. Open a snapshot for the plan's region and vehicle profile:

    POST /v1/plan/matrix
    {"region": "NA", "profile": "car"}
    

    The answer holds the snapshot id in matrix, its expires_at and the node that holds it.

  2. Send the id as matrix with every plan call of the same region and profile:

    {"vehicles": [...], "jobs": [...], "routes": [...], "matrix": "tile2.3f0c9a5e7b1d4c2a9e8f6d5c4b3a2910"}
    
  3. Close it when the plan is no longer edited: DELETE /v1/plan/matrix/{matrix}.

The answer of a call with matrix is the same as the answer without it, byte for byte, and is billed the same. Only the time changes: legs already measured are read from the snapshot.

The response header X-Plan-Matrix tells what happened to the snapshot:

Value Meaning
used Legs were read from the snapshot and the new ones were added to it.
expired The snapshot no longer exists. The call was measured without it. Open a new one.
unavailable The snapshot could not be reached. The call was measured without it.
mismatch The snapshot belongs to another region, profile or account, or the id is not a snapshot id. The call was measured without it.

A snapshot never makes a call fail: whatever happens to it, the call is answered as without matrix.

Limits

  • A snapshot is removed one hour after its last use, and eight hours after it was opened at the latest.
  • An account holds a limited number of snapshots at a time, and a snapshot holds at most 2,000 points. A refused opening answers SNAPSHOT_LIMIT_REACHED (429) with the limit reached in limit: snapshots_per_business, points_per_snapshot or node_memory.

Opening and closing a snapshot are free. They are available to an account with any plan capability, and to any API key granted one.

What a snapshot holds

The points of the plan and the road legs measured between them: distance, duration and the line of each leg. No order, address, vehicle or other data is kept. The snapshot is held in memory only, and everything in it is removed when it is closed or expires.

Editing in the browser or app

A plan editor in a web page or a mobile app calls the plan endpoints directly. The map SDKs include one, set up step by step in The plan editor. It must not hold your API token, so it uses one of two credentials:

Credential How it is obtained Where it works
Plan editor token (sr_pet_…) Your server calls POST /v1/plan/token with the API token and passes the answer to the editor. From the origins given when it was issued; from anywhere when none were given.
Map key (smk_…) Switch on Allow plan editing for the key on the Map keys page of the portal. From the web origins and apps on the key's allowlist.

Both are sent as Authorization: Bearer <credential>. A native app also sends X-SMap-App (and X-SMap-App-Cert on Android), as the map SDKs do.

POST /v1/plan/token
Authorization: Bearer <API token>

{"ttl_seconds": 900, "origins": ["dispatch.example.com"]}

The answer holds access_token, expires_in, expires_at, scopes and origins. A token is valid for 15 minutes by default and for one hour at most. Request a new one before it expires; a token cannot issue another token.

  • Both credentials call the plan endpoints only, POST /v1/plan/token excepted: re-costing, validation, the edit calls, the search calls and distance snapshots. Every other endpoint refuses them with NOT_AUTHORIZED, reason plan_routes_only.
  • A token carries the plan capabilities of its API key, and every call checks the key again. Revoking the key ends the token (SESSION_REVOKED, reason key_revoked), and so does a change of the account's capabilities (reason entitlements_changed). An expired token answers SESSION_EXPIRED.
  • A map key works only while the account holds the plan capabilities. Plan editing is off for every key until it is switched on.
  • A call from another origin or app answers NOT_AUTHORIZED, reason origin_not_allowed.
  • Calls are billed, limited and checked exactly as calls made with the API token.
  • The plan endpoints answer the browser's preflight. An answer to a call made with either credential carries Access-Control-Allow-Origin for the calling page and exposes X-Plan-Matrix. An answer to a call made with the API token never does: keep the API token on your server.