Live Solve Tracking
Set options.live_tracking = true on a POST /optimize/async request. While the job is running, GET /jobs/{job_id} returns a progress field with the solver's current best cost and served/unserved counts. If you cancel a live-tracked job, POST /jobs/{job_id}/cancel returns the best plan found so far as result instead of null, provided the solver reached at least one snapshot first. Default is false; not supported for sync endpoints.
Why this exists
An async solve can legitimately run for tens of seconds on a hard problem. Without this feature, a polling client sees exactly two states: running (no information at all about how the solve is progressing) and a terminal state once it finishes. Cancelling a running job discarded everything the solver had found — result came back null even if the solver had already converged on a good, usable plan seconds before the cancel request arrived.
Live solve tracking closes both gaps: you can watch a solve's current-best cost and served/unserved counts while it runs, and if you cancel, you get the best plan found so far instead of nothing.
Opting in
Set options.live_tracking = true on the request:
{
"resources": [ ... ],
"jobs": [ ... ],
"travel": { ... },
"options": {
"time_limit_seconds": 25,
"live_tracking": true
}
}
Default is false. A request that doesn't opt in behaves exactly as before this feature existed — no extra overhead, and the response's progress field is always null.
Polling while a job is running
GET /jobs/{job_id} includes a progress field, which is non-null only while status is "running" and the request had options.live_tracking = true:
{
"job_id": "c01508ef-91a2-4f5d-9caf-67543a5a10c2",
"status": "running",
"result": null,
"progress": {
"cost": 43.0,
"served": 11,
"unserved": 0,
"total_jobs": 11
}
}
Once the job reaches a terminal state, progress reverts to null — the real result field is what matters then.
Progress is written by a callback inside the solver every time it accepts an improved solution, and read independently whenever you poll. Two polls seconds apart may show the same snapshot if the solver hasn't improved on its current best in between — that's expected, not a bug.
curl example
# Submit with live tracking enabled
curl -X POST https://api.fieldgenius.be/api/v1/optimize/async \
-H "Content-Type: application/json" \
-H "X-API-Key: your-adapter-key" \
-H "X-Client-Key: your-client-key" \
-d @request.json
# -> {"job_id": "c01508ef-...", "status": "running", ...}
# Poll for progress
curl https://api.fieldgenius.be/api/v1/jobs/c01508ef-... \
-H "X-API-Key: your-adapter-key" \
-H "X-Client-Key: your-client-key"
# -> status: "running", progress: {"cost": 43.0, "served": 11, ...}
# Poll again a few seconds later -- cost may have improved, or stayed
# the same if no better solution was found in between
curl https://api.fieldgenius.be/api/v1/jobs/c01508ef-... \
-H "X-API-Key: your-adapter-key" \
-H "X-Client-Key: your-client-key"
Python example (httpx)
import time
import httpx
BASE = "https://api.fieldgenius.be/api/v1"
HEADERS = {
"X-API-Key": "your-adapter-key",
"X-Client-Key": "your-client-key",
}
request_payload = {
"resources": [...],
"jobs": [...],
"travel": {...},
"options": {"time_limit_seconds": 25, "live_tracking": True},
}
with httpx.Client(timeout=30, headers=HEADERS) as client:
submit = client.post(f"{BASE}/optimize/async", json=request_payload)
submit.raise_for_status()
job_id = submit.json()["job_id"]
while True:
job = client.get(f"{BASE}/jobs/{job_id}").json()
if job["status"] != "running":
break
if job.get("progress"):
p = job["progress"]
print(f"cost={p['cost']} served={p['served']}/{p['total_jobs']}")
time.sleep(2)
print("final status:", job["status"])
Cancel with salvage
POST /jobs/{job_id}/cancel on a job that opted into live tracking returns the last captured snapshot as result instead of null — provided at least one full snapshot had already been captured (roughly every five seconds) before the cancel request landed.
curl -X POST https://api.fieldgenius.be/api/v1/jobs/c01508ef-.../cancel \
-H "X-API-Key: your-adapter-key" \
-H "X-Client-Key: your-client-key"
{
"job_id": "c01508ef-...",
"status": "cancelled",
"result": {
"status": "feasible",
"routes": [ { "resource_id": "truck-1", "activities": [ ... ] } ],
"warnings": [
"This is a partial solution captured mid-solve (live tracking), ..."
]
}
}
The salvaged result is best-effort: if no snapshot was ever captured (for example, cancelled before the solver found its first improving solution), the job simply stays cancelled with result: null, exactly like an ordinary, non-live-tracked cancel.
The salvaged snapshot is a partial solution, not the solver's final answer. Stop timings in it are estimated from route order and travel time rather than the solver's own committed schedule, and don't account for time-window waiting — the response's warnings field always says so explicitly, as shown above.
Why sync endpoints aren't supported
Live solve tracking does not work for the synchronous endpoints: /optimize/sync, /optimize/replan, and /optimize/reoptimize. Two independent reasons, either of which alone would rule it out:
- No cancel mechanism. A sync caller holds one HTTP request open and waits for the single response; there is nothing to cancel into — half of this feature's value (salvage on cancel) has no sync equivalent to attach to.
- No clean way to push multiple updates over one HTTP response. Sending several updates down an already-open sync response would require chunked transfer encoding or switching the endpoint to an SSE-style response format — either breaks the existing "call and get one JSON object back" contract every current integration already depends on.
If you want live-tracking-like behavior for what feels like a synchronous call, the async endpoints plus polling already provide it: submit via /optimize/async with live_tracking: true and poll GET /jobs/{job_id} as fast as you want — this is strictly more capable than sync could offer, since it also gets cancel-with-salvage for free.
Configuration
No dedicated settings. Live tracking rides entirely on the same Redis-backed infrastructure as result caching: RESULT_CACHE_ENABLED (default true) gates every Redis write this feature makes, exactly as it gates the request cache and in-flight deduplication. Disabling it turns live tracking into a silent no-op — the progress field stays null and cancel never salvages a result — rather than an error.
- Result Caching — the Redis-backed cache this feature shares its infrastructure with.
- Webhooks & Async — the submit/poll/retrieve cycle live tracking builds on.
- Async Jobs reference — full
GET /jobs/{job_id}and cancel endpoint details. - POST /optimize/async reference