API Documentation
Everything needed to call Énroute programmatically — from a script, a BI tool, or an AI agent. Runs go through the same pay-as-you-go credit balance as the web app (1 optimization = 1 credit); generate a key from the Analytics page.
Authentication
Every request needs your API key as a bearer token. Keys are shown once, at creation — store it somewhere safe (it can only be revoked and replaced, never re-displayed).
Authorization: Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxxBase URL: https://routeoptimizerglobal.theagenticuniverse.com
Input formats
POST /api/v1/optimize accepts either shape — pick whichever fits how your data already lives.
1. Files (multipart/form-data)
Same fields as the upload form: combined_file and truck_file as actual CSV files, plus the param fields below.
2. JSON (dicts / records)
Already have your data as objects, not files? Send Content-Type: application/json with combined and truck as arrays of flat row objects — converted to CSV internally, same pipeline either way. Every row's keys become CSV columns, so use the exact column names from the Guide's file-format section (Doc No, Company Name, TotalWeight, GEOLAT, GEOLONG, Operating Hours, etc. for orders; truck_name, tonnage, start_time, truck_segment for trucks). Add an optional email to a truck row and that driver is emailed their delivery plan — assigned stops in order plus a Google Maps route link — once the run completes.
{
"combined": [
{ "Doc No": "4512560001", "Company Name": "EXAMPLE STORE SDN BHD",
"TotalWeight": 110, "GEOLAT": 2.0154, "GEOLONG": 102.5371,
"Operating Hours": "9.00-18.00", "UDF_UnloadTime": 30 }
],
"truck": [
{ "truck_name": "VDF7672", "tonnage": 12, "start_time": "07:00", "truck_segment": "high",
"email": "driver1@example.com" }
],
"country": "MY",
"origin_lat": 3.1578,
"origin_long": 101.7123,
"max_distance_km": 100,
"distance_mode": "route_length",
"filename": "morning_run"
}ℹ Param fields (country, origin_lat, origin_long, max_distance_km, distance_mode, filename, only_due_today) are identical in both modes.
cURL
Files:
curl -X POST https://routeoptimizerglobal.theagenticuniverse.com/api/v1/optimize \
-H "Authorization: Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-F "combined_file=@orders.csv" \
-F "truck_file=@trucks.csv" \
-F "country=MY" -F "origin_lat=3.1578" -F "origin_long=101.7123" -F "max_distance_km=100"JSON:
curl -X POST https://routeoptimizerglobal.theagenticuniverse.com/api/v1/optimize \
-H "Authorization: Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d @payload.jsonPython
import requests
API_KEY = "rok_live_xxxxxxxxxxxxxxxxxxxxxxxx"
BASE = "https://routeoptimizerglobal.theagenticuniverse.com"
payload = {
"combined": [
{"Doc No": "4512560001", "Company Name": "EXAMPLE STORE SDN BHD",
"TotalWeight": 110, "GEOLAT": 2.0154, "GEOLONG": 102.5371,
"Operating Hours": "9.00-18.00", "UDF_UnloadTime": 30},
],
"truck": [
{"truck_name": "VDF7672", "tonnage": 12, "start_time": "07:00", "truck_segment": "high"},
],
"country": "MY",
"origin_lat": 3.1578,
"origin_long": 101.7123,
"max_distance_km": 100,
}
res = requests.post(
f"{BASE}/api/v1/optimize",
headers={"Authorization": f"Bearer {API_KEY}"},
json=payload,
timeout=300,
)
res.raise_for_status()
result = res.json()
print(result["summary"])
for row in result["rows"]:
print(row["company_name"], row["cluster_label"], row["allocation_status"])Or with files instead of dicts: pass files={"combined_file": open("orders.csv","rb"), "truck_file": open("trucks.csv","rb")} and data={...param fields...} instead of json=.
JavaScript / Node
const res = await fetch("https://routeoptimizerglobal.theagenticuniverse.com/api/v1/optimize", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ROUTEOPTIMIZER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
combined: [
{ "Doc No": "4512560001", "Company Name": "EXAMPLE STORE SDN BHD",
TotalWeight: 110, GEOLAT: 2.0154, GEOLONG: 102.5371,
"Operating Hours": "9.00-18.00", UDF_UnloadTime: 30 },
],
truck: [
{ truck_name: "VDF7672", tonnage: 12, start_time: "07:00", truck_segment: "high" },
],
country: "MY",
origin_lat: 3.1578,
origin_long: 101.7123,
max_distance_km: 100,
}),
});
if (!res.ok) throw new Error((await res.json()).detail);
const { summary, rows } = await res.json();
console.log(summary);Power BI
Power Query's Web.Contents can POST directly and hand the JSON response straight to Json.Document — no custom connector needed. In Power BI Desktop: Get Data → Blank Query → Advanced Editor, paste:
let
payload = Json.FromValue([
combined = combinedRows, // a list of records you've built elsewhere in the query, or Table.ToRecords(YourOrdersTable)
truck = truckRows,
country = "MY",
origin_lat = 3.1578,
origin_long = 101.7123,
max_distance_km = 100
]),
response = Json.Document(Web.Contents(
"https://routeoptimizerglobal.theagenticuniverse.com/api/v1/optimize",
[
Headers = [#"Authorization" = "Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxx", #"Content-Type" = "application/json"],
Content = payload
]
)),
rowsTable = Table.FromList(response[rows], Splitter.SplitByNothing(), null, null, ExtraValues.Error),
expanded = Table.ExpandRecordColumn(rowsTable, "Column1",
{"cluster_label", "truck_name", "company_name", "allocation_status", "estimated_arrival", "window_violation"})
in
expandedAdjust the expanded column list to whichever output fields you want as Power BI columns — the full list is in the Guide's output-columns section. Schedule refresh in the Power BI Service like any other Web source.
Tableau
Tableau doesn't have a direct "call any REST API" data source the way Power BI does, so the practical path depends on how live you need it:
- One-off / periodic refresh — call the API (via curl, Python, or the examples above), save the JSON response, and open it directly with Tableau's built-in JSON connector (Connect → To a File → JSON File) — Tableau auto-detects the
rowsarray as a table. - Scheduled refresh — wrap the same call in a small script run on a schedule (cron, Task Scheduler, GitHub Actions) that overwrites the saved JSON file, and point a Tableau Bridge / scheduled extract at it.
- Live, published to Tableau Server/Online — use the Python example above together with Tableau's Hyper API to write the result rows straight into a
.hyperextract and publish it via the Tableau REST API — this is the right approach for a fully automated pipeline.
MCP — Claude, Cursor, and other AI agent tools
Énroute runs a standard Model Context Protocol server at /api/mcp (Streamable HTTP transport), exposing one tool — optimize_routes — that any MCP-compatible client can call directly, with the same JSON row-object input as above. No file handling on the agent's side at all.
Add a remote MCP server pointed at:
https://routeoptimizerglobal.theagenticuniverse.com/api/mcpwith header Authorization: Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxx. For clients that configure MCP servers via JSON (Claude Desktop, Cursor, and most agent frameworks use this shape):
{
"mcpServers": {
"routeoptimizer": {
"url": "https://routeoptimizerglobal.theagenticuniverse.com/api/mcp",
"headers": {
"Authorization": "Bearer rok_live_xxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}Once connected, just ask the agent to optimize a route — it will call optimize_routes with your order/truck data and read back the clustered result, same credit-gated pay-as-you-go balance as everywhere else.
Errors & status codes
| Status | Meaning |
|---|---|
| 200 | Success — { summary, rows }. summary.briefing is a short AI-generated plain-English explanation of the plan, when generation succeeded. |
| 400 | Malformed request — missing required fields, or 'combined'/'truck' aren't arrays of flat objects. |
| 401 | Missing or invalid API key. |
| 402 | Out of credits — { code: "out_of_credits", balance }. Top up from the Analytics page. |
| 422 | The optimizer ran but produced no result — the detail message explains why (usually a missing/misnamed column). |
| 502 / 504 | The optimizer backend failed or took too long. Inputs are still saved; check History before retrying. |
ℹ A run typically takes 1–2 minutes — keep the connection open, or (for very slow clients) poll History for the run instead.