JustRouting Python Client
Official Python client for the JustRouting API — routing, distance matrices, geocoding, map matching, trips, nearest-road lookup, and vehicle routing optimization across Southeast Asia.
No dependencies outside the standard library. Requires Python 3.10+.
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 in Singapore. See Coordinates must share a country.
Services
A Client exposes eight 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.
Geocode
Turn addresses into coordinates, free-form or structured:
results = client.geocode.search(justrouting.GeocodeRequest(
text="Marina Bay Sands, Singapore",
limit=3,
filters=["countrycode:sg"], # one filter per entry
))
r = results.results[0]
print(r.formatted) # "Marina Bay Sands, 10 Bayfront Avenue, 018956, Singapore"
print(r.location()) # a Point: [103.859, 1.2834]
print(r.rank.confidence)
structured (an address as typed fields) is mutually exclusive with text; bias steers results toward a location. An empty results list means nothing matched — it is a valid answer, not an error.
Map Matching
Snap a noisy GPS trace onto the road network:
match = client.map_matching.get(justrouting.MapMatchingRequest(
coordinates=trace, # GPS points, in chronological order
timestamps=ts, # required: UNIX seconds, one per point
radiuses=rs, # optional: max snap distance per point, metres
))
print(f"{match.confidence * 100:.0f}% confidence") # 0 to 1
Match extends Route, so distance, duration, geometry, and legs are all available. get_all also returns tracepoints aligned with the input coordinates — None entries mark points that could not be matched. Use gaps="ignore", tidy=True, waypoints=[0, 5], or snapping="any" to shape the matching; bearings, hints, approaches, exclude, generate_hints, and skip_waypoints are also supported.
Trip
The fastest order to visit a set of coordinates:
resp = client.trip.get_all(justrouting.TripRequest(
coordinates=[depot, stop_a, stop_b],
roundtrip=False, # default (None) ends where it started
))
trip = resp.trips[0]
print([wp.name for wp in resp.waypoints]) # in visiting order
source="first" / destination="last" pin where the trip starts and ends.
Nearest
The road segment closest to a coordinate:
wp = client.nearest.get(justrouting.NearestRequest(
coordinate=[103.8198, 1.3521],
number=3, # 2nd- and 3rd-nearest segments too
))
print(wp.name, wp.location, wp.distance) # street, snapped point, metres
print(wp.nodes) # OSM node IDs, when present
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 |
When the engine finds nothing — an empty matchings, trips, or waypoints list on an "Ok" response — map matching, trip, and nearest raise Error with status_code == 200 and osrm_code "NoMatch", "NoTrip", or "NoSegment" (no dedicated subclass; check e.osrm_code).
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/geocode.py
python examples/map_matching.py
python examples/trip.py
python examples/nearest.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
Metadata
Release files for justrouting 0.3.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| justrouting-0.3.2.tar.gz | 57.3 kB | Without provenance | Details for justrouting-0.3.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| justrouting-0.3.2-py3-none-any.whl | Python 3 | none | any | Details for justrouting-0.3.2-py3-none-any.whl |
Total release size: 105.6 kB
Release files / justrouting-0.3.2.tar.gz
| Download URL | justrouting-0.3.2.tar.gz |
|---|---|
| Size | 57.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e417972644d032df23e2fad91f3e1663fd7a71a71303b05fcb5f9ef6701cc27e
|
|
BLAKE2b-256 checksum How to use checksums |
c05c341a0afe86290aecdc85a3dee05bdc9257d292eaa7419568d159b3d3ed79
|
| 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.3.2-py3-none-any.whl
| Download URL | justrouting-0.3.2-py3-none-any.whl |
|---|---|
| Size | 48.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c00683928ab47f74afb9af0e85da822fb653e845863d3980cc5c8767f744b93
|
|
BLAKE2b-256 checksum How to use checksums |
0ba065daa56bd8fc7d03c07572e8e673df387da75f57753880b537c6dfe8170d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for justrouting 0.3.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.