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_xxxxxxxxxxxxxxxxxxxxxxxx

Base 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.json

Python

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
  expanded

Adjust 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 rows array 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 .hyper extract 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/mcp

with 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

StatusMeaning
200Success — { summary, rows }. summary.briefing is a short AI-generated plain-English explanation of the plan, when generation succeeded.
400Malformed request — missing required fields, or 'combined'/'truck' aren't arrays of flat objects.
401Missing or invalid API key.
402Out of credits — { code: "out_of_credits", balance }. Top up from the Analytics page.
422The optimizer ran but produced no result — the detail message explains why (usually a missing/misnamed column).
502 / 504The 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.