Wherebox
Get API key
On this page

Errors and limits

Every failure is an RFC 7807 application/problem+json response. Images and errors never mix: if you asked for a PNG and the status is not 200 (or 304), the body is JSON.

The shape#

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://wherebox.dev/docs/errors#invalid-spec",
  "title": "Invalid map spec",
  "status": 400,
  "detail": "The request does not match the map spec",
  "issues": [{ "path": "fade", "message": "…" }]
}

type is a stable code, title a short human label, status repeats the HTTP status, and detail says what happened. Some errors add fields: issues on 400, suggestions on 404, candidates on 409. The trailing part of type (after #) is the code, and every code has an entry in the error reference, which is generated from the same table the server uses. The part before # is the address of that entry in the published docs; a self-hosted server can point it at its own copy with WHEREBOX_DOCS_URL, so match on the code, not on the whole URL.

Branch on type, not on text#

title and detail are for people and may be reworded. type and status are the contract. A client usually needs three branches:

const res = await fetch(url);
if (res.ok) return res;                       // 200 image, or 304 with If-None-Match

const problem = await res.json();
const code = problem.type.split('#')[1];      // "ambiguous", "busy", "invalid-spec", …

switch (code) {
  case 'ambiguous':     // 409: let the user pick problem.candidates[i].id, retry with area=<id>
  case 'not-found':     // 404: offer problem.suggestions
  case 'invalid-spec':  // 400: a bug in your request; log problem.issues
  case 'busy':          // 503: retry, see below
  default:              // upstream-failed/-timeout (502/504): retry later or fall back
}

Which code means what, in one line each:

Status Code What to do
400 invalid-spec Fix the request. issues[] lists each bad parameter by path; unknown parameters come with a "did you mean" hint. Not retryable.
400 bad-request Something else is unacceptable as sent: a rejected icon or font, an unsupported extension, routing not configured. Not retryable.
404 not-found No such area or place. Use suggestions, or look it up with /v1/areas/search.
409 ambiguous Several areas match; see Locator maps.
422 no-route The routing engine found no route for that profile.
503 busy The render queue is full. Retry.
502 upstream-failed A basemap, tile or routing server failed. Retry later.
504 upstream-timeout A routing or asset server was too slow. Retry later.

Ambiguous names in practice#

A name that matches several areas is the one error you should expect in normal use. This request is parsed in the tests, but it is 409 when sent:

GET/v1/map.png
GET /v1/map.png?area=Nová+Ves&w=400&h=300

The response carries a candidates array; each entry has an id, an adminLevel and a context. Show the contexts to a person, then repeat the request with area=<id>. Or narrow it up front with level and country.

Invalid parameters#

Unknown parameters are rejected rather than ignored, so a typo cannot silently produce a different map. GET /v1/map.png?aera=Czechia returns 400 invalid-spec with an issue like unknown parameter "aera" (did you mean "area"?). Out-of-range numbers and malformed colors are reported the same way, each with its path.

Retries on 503#

When more renders are requested than the server can run and queue, it answers 503 with busy and a Retry-After header (in seconds). Retry after that long, with a little jitter, and give up after a few attempts. Retry 502 and 504 the same way. Do not retry 400, 404, 409 or 422: the answer will not change.

Identical concurrent requests share a single render, and a repeated request is served from the cache without taking a render slot, so retry storms on one URL are cheap.

async function fetchMap(url, tries = 4) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch(url);
    if (res.status !== 503 && res.status !== 502 && res.status !== 504) return res;
    const wait = Number(res.headers.get('retry-after') ?? 2) * 1000 * (i + 1);
    await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
  }
  throw new Error('map service unavailable');
}

The queue size and concurrency are operator settings; see the configuration reference.

Caching and ETags#

Maps are content-addressed: the cache key is a hash of the whole request (after defaults are applied) plus the renderer and data versions. The same request always gives the same image until the server's data or renderer changes.

  • Image responses carry ETag (the hash), Cache-Control: public, max-age=86400 and X-Cache: HIT|MISS. Send If-None-Match with the ETag to get an empty 304 when nothing changed.
  • GET responses can sit behind a CDN or the browser cache as they are. Because the key covers every parameter, a changed parameter is a new URL and a new image; there is nothing to purge.
  • POST /v1/map?response=json returns {url, key, meta}. The url (/v1/images/{key}) is immutable (max-age=31536000, immutable), so store it or put it in a page. If the cached image has expired, that URL returns 404 not-found; re-send the POST to render it again.
  • Two headers describe the result: X-Image-Width and X-Image-Height (the real pixel size, which can exceed w × scale with neighbor extend) and X-Render-Ms on a fresh render.

Parameters beginning with an underscore (&_v=2) are ignored by the parser. Use one if you need a different URL for the same map, for example to get past a stale browser or CDN entry; the server still returns the same cached image.