Best Practices
TL;DR: validate before solving in production paths, calibrate penalties relative to your matrix scale, prefer named multi-capacity over single-int shorthand once you have more than one dimension, and use diagnostics liberally while building a new integration.
Validate before you solve
Call POST /optimize/validate before submitting a request you have not tested, especially from a UI or pipeline that constructs requests programmatically. It runs all semantic checks (referential integrity, duplicate IDs, matrix shape, time window ordering, circular precedence) without invoking the solver, so it is fast and returns a clear 422 with the exact problem instead of a slow timeout or a confusing solver error.
Enable diagnostics while developing
Set output_options.include_diagnostics: true while building a new integration. If a request comes back infeasible, the infeasibility_report explains exactly which constraint type is the bottleneck (skills, time windows, capacity, etc.) instead of leaving you to guess. Turn it off in production once your request shapes are stable, since it adds a small amount of extra solver-side computation.
Calibrate penalty magnitudes
Penalties, soft time-window violation costs, and revenue are all added directly into the solver's objective, in the same units as your travel matrices. A penalty: 100000 next to a matrix where typical arc costs are 5-50 means the solver will almost never drop that job. Pick penalty/cost magnitudes deliberately rather than relying on the default (100,000), especially once you mix optional jobs, soft time windows, and multiple objectives in the same request.
Use named capacities once you have more than one dimension
The single-integer shorthand for capacities/demands is convenient for the common single-dimension case, but switch to the named dict form ({"weight": 100, "volume": 4}) as soon as a second dimension appears. Mixing shorthand and named forms across resources in the same request is not supported.
Set a realistic time budget
The default options.time_limit_seconds is 30. Small problems (under ~50 jobs) typically converge well within that. Larger or more constrained problems benefit from a longer budget, multi-phase solving (options.phases), or letting the auto-optimizer choose a phase recipe by omitting options entirely.
Use problem_id for traceability
Always set metadata.problem_id to a stable identifier you control. It is echoed back in the response and appears in structured logs, making it much easier to correlate a request with its result when debugging production issues.
Use async for anything that might exceed your HTTP timeout
If your time budget plus the 120-second solver grace period could exceed your client or load balancer's timeout, use /optimize/async and poll instead of /optimize/sync.
Use replan, not a fresh solve, for mid-day changes
When a vehicle is already executing a route and you need to react to a cancellation, a new urgent job, or simply re-optimize the remainder of the day, use POST /optimize/replan rather than constructing a brand new request from scratch. It correctly advances vehicle positions and times and excludes completed work without you having to reconstruct that state manually.