On this page
Python SDK
wherebox is a small Python client for the API: it builds and signs map URLs for <img> tags, renders images to bytes or files, and looks up areas and places. It uses only the standard library and works on Python 3.9 or newer.
pip install whereboxFrom a checkout of this repository (to try unreleased changes):
pip install ./sdk/pythonQuick start#
from wherebox import Client
client = Client(
"https://maps.example.com",
secret_key="sk_…", # server-side calls (render, lookups)
publishable_key="pk_…", # embeds
signing_secret="<hex signing secret>",
)
spec = {
"area": "Czechia",
"pois": [{"place": "Chřiby", "label": "Chřiby", "marker": {"shape": "triangle"}}],
"neighbors": {"show": "flag+code"},
"width": 468,
"height": 280,
}
img_src = client.url(spec) # a signed GET URL for <img src="…">
result = client.render(spec) # PNG bytes, rendered by POST /v1/mapA spec is the JSON body of POST /v1/map as a Python dict: see the request body reference. The same spec works for url() and render(). Names, defaults and ranges are in the reference; the guides explain what each part does (locator maps, points and labels, routes).
Every key is optional. A self-hosted server without WHEREBOX_AUTH=keys needs none, and url() then returns an unsigned URL.
Signed URLs for embeds#
A page or email can only show a map through a URL, and a publishable key is only accepted with a signature. Client.url() builds the URL from the spec and signs it. Do this on your server: the signing secret must never reach a browser.
url = client.url(spec, format="webp", expires_in=3600) # valid for an hourexpires_in is optional (seconds); the expiry is covered by the signature. Without it the URL works until you rotate the key. url() raises PostOnlyError when the spec uses something a URL cannot carry (shapes, a second route, a GeoJSON area, a custom font, a | in a label); its paths attribute lists the offending fields. Render those with client.render(), which uses POST.
To sign a URL you built yourself, use sign_url(url, publishable_key, signing_secret, expires_at=None). The output is identical to the server's own signUrl (a cross-language test checks it) and to the snippet in the authentication guide.
Django#
from django.shortcuts import render
from wherebox import Client
maps = Client("https://maps.example.com", publishable_key=settings.WHEREBOX_PK, signing_secret=settings.WHEREBOX_SIGNING_SECRET)
def office(request):
src = maps.url({"area": "Czechia", "pois": [{"place": "Brno", "label": "Our office"}], "width": 468, "height": 280}, expires_in=3600)
return render(request, "office.html", {"map_src": src}) # <img src="{{ map_src }}" width="468" height="280" alt="Map">Flask#
from flask import Flask, redirect
from wherebox import Client
app = Flask(__name__)
maps = Client("https://maps.example.com", publishable_key=PK, signing_secret=SIGNING_SECRET)
@app.get("/maps/<city>.png")
def city_map(city):
# Sign on demand and redirect: the browser fetches the image from Wherebox, not through your server.
return redirect(maps.url({"area": "Czechia", "pois": [{"place": city}], "width": 468, "height": 280}, expires_in=300))Rendering to a file#
from wherebox import Client
client = Client("https://maps.example.com", secret_key="sk_…")
result = client.render(spec, format="png")
with open("czechia.png", "wb") as f:
f.write(result.image)
print(result.content_type, result.width, result.height, result.cached)RenderResult has image (bytes), content_type, width and height (the final size from X-Image-Width / X-Image-Height, which can exceed width × scale when neighbor labels extend the canvas), cached (the server answered from its cache) and etag. client.render_json(spec) returns the JSON form instead: a durable url for the cached image, its key and size.
Keys for a commercial basemap or a routing provider go in headers, not in the spec, so they stay out of logs: client.render(spec, provider_key="…", routing_key="…"). See HTTP: headers.
Lookups share the client:
client.search_areas("Zlín", limit=5) # list of dicts with id, name, adminLevel, context
client.search_places("Chřiby")
client.get_area("osm:r51684")Errors#
Every failed request raises WhereboxError, built from the problem+json body:
| Attribute | Meaning |
|---|---|
status |
HTTP status (0 when the server could not be reached; code is then network) |
code |
The catalog code, e.g. "ambiguous"; see the error reference |
type, title, detail |
The RFC 7807 fields; detail says what happened |
issues |
invalid-spec: [{path, message}] per problem |
candidates |
ambiguous: matching areas with id, adminLevel and context |
suggestions |
not-found: close matches |
retry_after |
Seconds from Retry-After (busy, rate-limited), else None |
retryable |
True for busy and rate-limited |
Branch on code:
from wherebox import WhereboxError
try:
result = client.render({"area": "Nová Ves"})
except WhereboxError as e:
if e.code == "ambiguous":
for c in e.candidates: # pick one, then retry with area=c["id"]
print(c["id"], c["context"])
elif e.code == "not-found":
print("did you mean:", [s["name"] for s in e.suggestions])
elif e.code == "invalid-spec":
for issue in e.issues:
print(issue["path"], issue["message"])
elif e.code in ("unauthorized", "forbidden"):
raise RuntimeError("check the API key") from e
else:
raiseRetries on 503 and 429#
busy (503) and rate-limited (429) are transient: wait retry_after seconds and try again. quota-exceeded is also a 429, but it lasts until next month or until the limit is raised, so retryable is False for it.
import time
from wherebox import WhereboxError
def render_with_retries(client, spec, attempts=4):
for attempt in range(attempts):
try:
return client.render(spec)
except WhereboxError as e:
if not e.retryable or attempt == attempts - 1:
raise
time.sleep(e.retry_after if e.retry_after is not None else 2 ** attempt)Cached images never hit the render queue, so identical requests are cheap to repeat. See errors and limits for caching and ETags.