JustRouting Python Client
Official Python client for the JustRouting API — routing, distance matrices, and vehicle routing optimization across Southeast Asia.
No dependencies outside the standard library. Requires Python 3.9+.
Install
pip install justrouting
Or install from a clone of this repository:
pip install -e .
import justrouting
Quickstart
import justrouting
client = justrouting.Client("YOUR_API_KEY")
route = client.routes.get(
justrouting.RouteRequest(
origin=[103.708362, 1.357371],
destination=[103.984748, 1.352212],
),
timeout=30,
)
print(f"Distance: {route.distance / 1000:.1f} km")
The coordinates above are Singapore and Kuala Lumpur, which span two countries. See Coordinates must share a country — the runnable examples use same-country pairs.
Services
A Client exposes four services.
Routes
Routes.get returns the best route. Routes.get_all additionally returns alternatives and the snapped input waypoints.
route = client.routes.get(justrouting.RouteRequest(
origin=[103.8198, 1.3521],
destination=[103.9915, 1.3644],
waypoints=[[103.8514, 1.2897]], # stops in order
overview="full", # full geometry
steps=True, # turn-by-turn
))
print(route.distance) # metres
print(route.duration) # seconds
route.geometry holds whichever encoding you asked for:
polyline = route.geometry.polyline() # default, and "polyline6"
line = route.geometry.geojson() # when geometries="geojson"
Matrix
Travel time and distance between many points at once.
m = client.matrix.get(justrouting.MatrixRequest(
coordinates=[depot, stop_a, stop_b],
sources=[0], # only the depot row; cheaper than N×N
destinations=[1, 2],
))
seconds = m.duration(0, 1)
if seconds is not None:
print(f"depot -> stopA: {seconds / 60:.0f} min")
The accessors return None for unreachable pairs. The API reports those as null, which is deliberately kept distinct from a genuine zero.
Optimization
Assign tasks to a fleet and order each vehicle's stops.
solution = client.optimization.solve(justrouting.OptimizationRequest(
vehicles=[justrouting.Vehicle(
id=1, start=depot, end=depot, capacity=[4],
)],
jobs=[
justrouting.Job(id=1, location=stop_a, delivery=[1], service=300),
justrouting.Job(id=2, location=stop_b, delivery=[2], service=300),
],
))
for route in solution.routes:
print(f"vehicle {route.vehicle}: {len(route.steps)} stops")
print(len(solution.unassigned), "task(s) could not be served")
Use Shipments instead of Jobs for pickup-and-delivery pairs that must be served in order by the same vehicle.
Health
The only call that works without an API key, which makes it a useful connectivity check.
health = client.health.get()
print(health.ok(), health.upstreams)
Error handling
Every failure raises a subclass of justrouting.Error. Classify it with except clauses rather than matching on message text:
try:
route = client.routes.get(req)
except justrouting.QuotaExceededError:
# daily allowance used up — retrying will not help
except justrouting.RateLimitedError:
# throttled; the client already retried
except justrouting.CrossCountryError:
# coordinates span more than one country
except justrouting.NoRouteError:
# no road connects these points
| Exception | Meaning |
|---|---|
UnauthorizedError |
API key missing, invalid, or revoked |
RateLimitedError |
Throttled (per-second limit or daily quota) |
QuotaExceededError |
Daily quota exhausted; retrying will not help (subclasses RateLimitedError) |
PlanLimitExceededError |
Too many matrix coordinates, jobs, or vehicles |
CrossCountryError |
Coordinates span more than one country |
InvalidCoordinatesError |
Coordinate malformed or out of range |
NoRouteError |
No route exists between the points |
UpstreamUnavailableError |
Routing engine unreachable; usually transient |
InvalidRequestError |
Rejected locally before any request was sent |
Catch justrouting.Error itself when you need the status code, engine code, or raw body:
except justrouting.Error as e:
print(f"HTTP {e.status_code}: {e.message}")
print(e.body) # raw response body, truncated
Failures that never produced an API response raise TransportError (network-level, retried like a 5xx) or DecodeError (unparseable success response) instead; neither is a subclass of Error.
Configuration
| Argument | Default | Purpose |
|---|---|---|
api_key |
— | Sent as Authorization: Bearer on every authenticated request |
base_url |
https://api.justrouting.tech |
Target a local or staging server |
user_agent |
justrouting-py/<version> |
Identify your application |
max_retries |
2 |
Retry budget on top of the initial attempt |
backoff |
500ms → 8s, jittered | Callable replacing the retry delay schedule |
timeout |
30 |
Seconds per HTTP attempt |
Invalid options raise ValueError immediately. (The Go client defers them to the first request because its constructor cannot fail; Python constructors can.)
Timeouts and retries
Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a Retry-After header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota.
Client(timeout=...) covers a single HTTP attempt, like http.Client.Timeout in Go. Pass the per-call timeout argument to bound the whole retry sequence:
route = client.routes.get(req, timeout=30) # 30s total, retries included
Proxy configuration follows the standard HTTP_PROXY / HTTPS_PROXY environment variables.
Things to know
Coordinates are [longitude, latitude]
This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the [-90, 90] check before a request is sent — but a swap that stays in range will silently route somewhere unexpected.
Point is a list subclass, so both of these work:
origin=[103.8198, 1.3521]
origin=justrouting.Point([103.8198, 1.3521])
Coordinates must share a country
Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with CrossCountryError.
Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam.
Plan limits
| Free | Hobby | |
|---|---|---|
| Requests per day | 100 | 10,000 |
| Requests per second | 5 | 10 |
| Matrix coordinates | 100 | 500 |
| Jobs per optimization | 100 | 1,000 |
| Vehicles per optimization | 10 | 50 |
Exceeding a size limit raises PlanLimitExceededError; exhausting the daily allowance raises QuotaExceededError.
Examples
Runnable scripts live in examples/:
export JUSTROUTING_API_KEY=<your key>
python examples/route.py
python examples/matrix.py
python examples/optimization.py
Development
pip install -e '.[dev]'
pytest
The default suite runs entirely against local http.server instances — no network access and no API key.
Integration tests hit a live API and are behind a marker, so they never run by accident:
pytest -m integration # health and auth only
JUSTROUTING_API_KEY=<key> pytest -m integration
JUSTROUTING_BASE_URL=http://localhost:8080 pytest -m integration
Differences from the Go client
- Exceptions replace sentinel errors and
errors.Is;QuotaExceededErrorsubclassesRateLimitedError. - Constructor options are keyword arguments; invalid ones raise
ValueErrorimmediately instead of being deferred to the first request. - There is no
context.Context; the per-calltimeoutargument plays the role of a context deadline. - Matrix accessors return
float | Noneinstead of(float, ok). - There is no custom-HTTP-client option; use
base_url,timeout, and the standard proxy environment variables instead.
License
Release files for justrouting 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| justrouting-0.1.1.tar.gz | 36.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| justrouting-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.1 kB
Release files / justrouting-0.1.1.tar.gz
| Download URL | justrouting-0.1.1.tar.gz |
|---|---|
| Size | 36.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c74ca67664abb7ade94c37e49f57676bbf62f66ff745ba3a9755f8468712aba6
|
|
BLAKE2b-256 checksum How to use checksums |
da3ea8456634fdd9d8e5b151cbeb74e2223a0e8397f4d05782de4d812aab9746
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / justrouting-0.1.1-py3-none-any.whl
| Download URL | justrouting-0.1.1-py3-none-any.whl |
|---|---|
| Size | 30.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e228b0cd0705e27d580b1305316230279db221decccf5243685e50196f3caea2
|
|
BLAKE2b-256 checksum How to use checksums |
754f6d062a11fd4b3acb4b6df28323be24b7e306a1cf9e9a8cca90565f2e60c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|