Errors
Shape
Most JSON endpoints return this body on failure:
{ "error": "human-readable message", "code": "machine-readable-code" }Both fields are always present. error names the parameter or the condition at fault in
words; code is the value to branch on, because the messages are written in English only and a
client should never have to parse prose to decide what to do.
Optimize uses Google's structured error format instead:
{ "error": { "code": 400, "message": "...", "status": "INVALID_ARGUMENT", "details": [] }, "code": "bad_request" }.
Read the top-level string code for Unmap's error category and error.status for Google's status.
The Optimize section below lists its statuses, validation codes, warnings and skip reasons.
Tile, terrain, contour, glyph, and sprite requests are the exception. Their error bodies are
plain text, not JSON: invalid tile coordinates, invalid fontstack, not found, and so on.
MapLibre GL never reads these bodies itself; it only checks the status code and moves on. Styles
are JSON too, except one case: when the active basemap release is not an eight-digit build id, or
that object is not in storage, both the style and the undated tile path answer plain text
503 basemap release is misconfigured with Cache-Control: private, no-store. That response
does not serve the previous archive. Every other endpoint on the gateway (geocoding, routing, and
the account API) returns JSON, as does the one JSON error the terrain and contour paths do send,
the 404 for an unconfigured archive.
The code union
type ErrorCode =
| "unauthorized" // 401: missing, malformed, or unrecognised key
| "rate_limited" // 429: over the per-key burst limit
| "quota_exceeded" // 429: dev plan at its monthly included volume
| "spend_cap" // 429: paid plan at its spend cap
| "billing_required" // 429: a failed payment on a paid plan
| "origin_not_allowed" // 403: this key's allowed-origins list does not cover the request
| "addon_required" // 403: paid plan, this key does not hold the named capability
| "dev_only" // 403: dev plan, premium capability, production origin
| "usage_limit_reached" // 403: dev plan past its monthly premium-call ceiling
| "bad_request" // 400: data plane when no more specific code applies, including a
// geocoding or places coordinate outside WGS84; the account API uses it too
| "invalid_theme" // 400: a `?theme=` code that does not decode or validate
| "invalid_coordinate" // 400: /timezone: lat or lon is missing, not a number, or outside its range
| "invalid_timestamp" // 400: /timezone: `at` is not an ISO 8601 instant
| "invalid_location" // 400: /route, /isochrone, /matrix, /match, /optimized-route: a coordinate
// is missing, malformed, or outside WGS84
| "invalid_mode" // 400: /route, /isochrone: an unknown travel mode
| "invalid_route_option" // 400: a truck dimension, avoid, use_highways, use_hills or a contour cap is
// out of range or unknown, or was sent on a mode whose costing has
// no such option; also avoid=highways sent with use_highways
| "invalid_body" // 400: /matrix, /match, /optimized-route: the body is missing, not JSON, or over the size cap
| "too_many_locations" // 400: /matrix, /match, or /optimized-route exceeded its origin, destination, point, or stop cap
| "outside_coverage" // 422: an anchor outside Canada. The graph and the geocoder are not called
| "no_route" // 422: /route, /matrix, /match, /optimized-route: a Canadian request the graph cannot answer
| "service_unavailable" // 503: a container-backed service did not answer in time, or is not
// configured on this deployment (the body then carries `contract`)
| "not_found" // 404: unknown style, layer, or place; an unconfigured terrain or contour
// archive; a path this gateway does not serve. The account API uses
// it for an unknown resource too
| "not_implemented" // 501: /data/query on a catalogued but tile-only layer
| "internal_error" // 500: an unexpected Optimize failure
| "deadline_exceeded" // 504: Optimize road travel or solver response exceeded its time budget
| "upstream_error" // 502: an upstream unmap depends on failed: the routing container, or
// a terrain tile /elevation cannot decode
| "invalid_token" // account API only: bad or expired magic-link / session token
| "conflict" // account API only: the job state does not allow this
| "payload_too_large" // 413: batch query count, Optimize body size or account upload exceeds its limit
| "plan_required"; // account API only: this needs a paid planEverything down to upstream_error is what your application's API-key traffic can hit. The last
four belong to the account dashboard's own endpoints (/account/*), which authenticate with a
browser session rather than an API key; payload_too_large and conflict are shared, because
POST /geocode/batch raises the first and hosted batch jobs raise both. A coded routing failure
lets a client branch on too_many_locations versus invalid_location instead of parsing the
message, and invalid_theme also carries a detail array naming what was wrong with the code.
See
Routing for which endpoint emits which routing code, and
ROUTING_ERROR_CODES in apps/gateway/src/routing/errors.ts for the source list.
On the data plane, payload_too_large is the 413 for POST /geocode/batch when queries
has more than 100 entries, and for Optimize when the body exceeds 4 MiB. Split the request;
do not retry the same oversized body. Optimize also uses internal_error for 500 and
deadline_exceeded for 504; see Optimize below. The account codes
conflict and plan_required belong to hosted batch geocoding, the dashboard's
session-authenticated job runner, not to POST /geocode/batch. There is no key-authenticated job
API; see Batch geocoding.
Headers a browser can read
Cross-origin JavaScript can only see response headers the server explicitly exposes, and a header
your code cannot read is a header that does not exist as far as your retry logic is concerned.
The API exposes Retry-After, ETag, Content-Length, Content-Range and Accept-Ranges, plus
Optimize's X-Unmap-Billable-Shipments, X-Unmap-Optimize-Warnings and Warning headers.
Retry-After is the one that matters here: it is on every 429 except billing_required, and on
the retryable 503, so a browser client can honour it rather than guessing at a backoff.
Status codes
401: missing, malformed, or unrecognised API key. Codeunauthorized. Every data-plane401also carries an RFC 6750WWW-Authenticatechallenge naming the scheme and pointing at the metadata that explains it:Bearer realm="api.unmap.dev", resource_metadata="https://api.unmap.dev/.well-known/oauth-protected-resource", witherror="invalid_token"added when a key was presented but not recognised.429: one of four things, told apart bycode:rate_limited: over the per-key burst limit (1,000 requests per 60 seconds per Cloudflare location).Retry-After: 60.quota_exceeded: a dev key past its 10,000 included calls for the month.Retry-After: 3600; the block lifts on the next monthly reset.spend_cap: a paid key at its configured spend cap.Retry-After: 3600; raise the cap in the dashboard.billing_required: the last payment on a paid plan failed. NoRetry-After, because no amount of waiting fixes it; update the payment method in the dashboard.
403: one of four things, told apart bycode:origin_not_allowed: the key carries a non-empty allowed-origins list and this request is not covered by it. A missingOriginheader, an unparseable one, and a well-formed origin that is simply not on the list all get this same answer, and the body never tells you what the list contains. A key with an empty list is unrestricted and never sees this. See Authentication.addon_required: a paid-plan key that does not hold the capability this layer or overlay needs. The body carriescapability(legalLand,energy,agriculture,remote,mining, orski),addonnaming the cheapest SKU that would grant it (addon-legal-land,addon-energy,addon-agriculture,addon-remote,addon-mining,addon-ski), and amessagewritten for a person whereerroris written for a log. See Layers. Keys without an account (founder and integration keys) are exempt.dev_only: a dev-plan key using a premium capability from a production origin. The same key is allowed fromlocalhost,127.0.0.1,*.localhost,*.pages.devand*.workers.dev, where every capability is free. It carries the samecapability,addonandmessagefields, but the fix is different and that is why the code is: a dev-plan account has no subscription for an add-on to attach to, so the step is to subscribe. See Layers.usage_limit_reached: a dev-plan key past its 5,000 premium calls for the month. The same key's core traffic is unaffected and keeps serving: only the gated capabilities refuse. It clears at the start of the next month, and a paid plan removes the ceiling entirely. Carries the samecapability,addonandmessagefields.
400: a malformed request parameter, codebad_requestunless a more specific one applies. Bad tile coordinates, a missingqon a geocoding call, a malformedbbox, a missing or non-numericnear, a longitude or latitude outside WGS84 on geocoding or places, an unknowncategory, both or neither ofnearandbboxon a category search, more than four isochrone contours or one over 120 minutes, an invalid theme code, an unknown/data/querylayer, and so on. The message string names the parameter.501:GET /data/queryon a catalogued tile-only layer (no PostGIS table). Codenot_implemented; the message lists the queryable ids. See Layers.404: an unknown style on/styles/{style}.json, an unknown layer or place,layer archive not configuredwhen a catalog layer's object is not in the bucket, a terrain or contour archive that is not configured, or a path the gateway does not serve. Those JSON bodies usenot_found, and so does an unknown resource on the account API. A missing key is401before any of these, including on an unknown path. A missing glyph range or sprite sheet, and an unmatched path under/tiles,/terrain,/contours,/layers,/glyphs, or/sprite, stay plain text.204: a tile request beyond the archive's zoom ceiling, or for a tile that holds no data. An empty answer, not a failure: MapLibre overzooms from the deepest tile it already has.422: the request was understood and refused. Two codes share this status, so branch oncode.outside_coverage: an anchor is outside Canada. The body is{ "error": "outside Canadian coverage", "code": "outside_coverage" }. Amode=transitisochrone whose centre has no scheduled transit nearby uses the same code with the errorno scheduled transit serves this point; use a road mode such as mode=pedestrian. The graph and the geocoder are not called./route,/isochrone,/matrix,/match,/optimized-route,/geocode/reverse,/geocode/nearbyand/places/nearbyall use it. A text search or autocomplete with no match stays an empty list. A longitude or latitude outside WGS84 is400bad_requeston geocoding and places, and400invalid_locationon routing. It is notoutside_coverage.no_route: the point is inside Canada and the graph still has no answer. On/routethe body is{ "error": "no route", "code": "no_route" }. Open water inside Canada, including Hudson Bay, and a trip the roads do not connect, including Iqaluit to the mainland, areno_route./matchusesno_routewhen a Canadian trace cannot be snapped to any road./matrixkeeps a pair the engine merely fails to connect as anullcell inside a200, with the rest of the grid intact, so handlenullon every cell. It answers422no_routewhen the engine refuses the request outright: a location with no road near it, locations that are not all in one connected region, or two locations further apart than its straight-line limit. A location outside Canada isoutside_coveragebefore that check./optimized-routefails whole for those same graph reasons, confirmed against the deployed gateway, though anullin its internal matrix alone does not fail it; see Routing.
502: an upstream answered with a failure of its own. Codeupstream_error: the routing container failed, or/elevationmet a terrain tile that will not decode.503: four different situations:-
The container did not answer at all, or did not answer inside the gateway's deadline. This is the one you may actually see in production, during a container restart. The body carries a
code, aretryableflag, and aRetry-After:{ "error": "geocode temporarily unavailable", "code": "service_unavailable", "retryable": true }The service name is
geocode,routing,landorenergy. Nothing about why is disclosed, deliberately: a container's own error text can name an image tag or a database message.Retry-Afterdiffers by service, and the difference is not cosmetic.geocodeandroutingare kept warm by a cron that pings them every five minutes, so their503is a blip and the header says5.landandenergy, which serve the legal-land, energy and mining surfaces, are allowed to sleep: their first request after an idle period boots Postgres on a baked data directory and takes about a minute, so the header says60. Honour the header. Retrying aland503every five seconds fails roughly eleven times before it can possibly work, which reads as a broken product rather than a cold start. -
The service is not wired up at all. You see this running the gateway locally without the containers attached, not in production. The body carries
code: "service_unavailable"and, instead ofretryable, repeats the endpoint's expected query shape, so a caller learns the contract from the failure:{ "error": "routing is not deployed yet", "code": "service_unavailable", "contract": "?from=lng,lat&to=lng,lat&mode=auto|car|bicycle|pedestrian|truck|transit", } -
The active basemap release is not an eight-digit build id, or that object is not in storage. The style and the undated tile path answer plain text, not JSON:
basemap release is misconfiguredCache-Controlisprivate, no-store. The previous archive is not served in its place, and another request does not bring it back. On/tiles/{release}/{z}/{x}/{y}, an eight-digit id that was never copied is404basemap release not found, alsoprivate, no-store. An id that is not eight digits is that same404body without the cache header.
-
Each API page lists its own 400 messages verbatim: the Maps API,
Geocoding, Routing.
The bodies, verbatim
The six failures you are most likely to meet, exactly as the gateway sends them. Every one of these is copied from a real response, not paraphrased.
Missing key, 401. No Authorization: Bearer, no X-API-Key, no X-Goog-Api-Key, no ?key=:
{ "error": "missing API key", "code": "unauthorized" }Invalid or disabled key, 401. The key is well-formed but not in the registry, or it has
been deleted. The two cases are deliberately indistinguishable from outside:
{ "error": "invalid or disabled API key", "code": "unauthorized" }Over a limit, 429. Four different situations behind one status, told apart by code.
rate_limited is the per-key burst limit and carries Retry-After: 60:
{ "error": "rate limit exceeded", "code": "rate_limited" }The other three are the monthly enforcement flags, written by the five-minute cron rather than
counted per request. quota_exceeded and spend_cap carry Retry-After: 3600; billing_required
carries no Retry-After at all, because no amount of waiting fixes a failed payment:
{
"error": "monthly quota exceeded: raise your spend cap or upgrade at unmap.dev/dashboard/billing",
"code": "quota_exceeded",
}
{ "error": "spend cap reached: raise it at unmap.dev/dashboard/billing", "code": "spend_cap" }
{
"error": "payment failed: update your payment method at unmap.dev/dashboard/billing",
"code": "billing_required",
}Unsupported routing mode, 400. /route and /isochrone validate mode rather than
falling back, so a typo is loud:
{ "error": "mode must be one of auto, car, bicycle, pedestrian, truck, transit", "code": "invalid_mode" }Invalid theme code, 400. The one data-plane 400 that carries a third field beside error
and code: a detail array saying what was wrong. A code that is not a ut1. document or a
short preset fails one way, and a ut1. code whose payload will not inflate fails another:
{
"error": "invalid theme",
"code": "invalid_theme",
"detail": ["code must start with \"ut1.\" or be a short preset"],
}
{
"error": "invalid theme",
"code": "invalid_theme",
"detail": ["code is not valid base64url deflate data"],
}Unknown style, 404. /styles/{style}.json lists the names that are real, so a bad one is
self-correcting:
{
"error": "unknown style",
"code": "not_found",
"styles": ["base", "muted", "outdoor", "blueprint", "blush", "orchid", "canopy", "lagoon", "tropic", "sunset", "bold", "pastel", "atlas", "swiss", "winter"],
}In the SDK
Every client throws on a non-2xx response, and every error extends UnmapError, so one
catch covers the whole platform:
import { Unmap, UnmapError } from "@unmap/sdk";
try {
await unmap.places.nearby("bakery", { near: [-114.07, 51.05] });
} catch (e) {
if (e instanceof UnmapError) {
console.error(e.status, e.code, e.message, e.retryAfterSeconds);
}
}Each product has its own subclass, which carries its own name, so
e instanceof GeocoderError narrows to one product: GeocoderError, PlacesError, RouterError,
ElevationError, RoadsError, TransitError and CoverageError. What each carries is the HTTP
status, the machine-readable code when the body had one, retryAfterSeconds when the
response carried the header, and the gateway's own error string as the thrown message, so a
caller who logs it reads the reason rather than only a number. A body that is not JSON, such as a
tile path's plain text, simply leaves code undefined. status is 0 when the request never
reached the server.
e.retryable is true for exactly the statuses worth waiting out: 429, 502, 503 and a
transport failure.
Map tiles, glyphs and sprites are fetched by MapLibre itself, which surfaces failures as error
events on the map rather than as thrown exceptions.
The clients retry for you
Every read retries 429, 502, 503 and a transport failure, three attempts by default, and
honours Retry-After when the response carries one rather than guessing at a backoff. Nothing
else is retried: a 400 or a 403 describes the request or the account, and repeating it only
spends quota.
new Unmap({ key, retry: false }) // off
new Unmap({ key, retry: { attempts: 5 } }) // more persistent
new Unmap({ key, retry: { maxDelayMs: 120_000 } }) // willing to wait out a cold containerA wait longer than maxDelayMs (10 seconds by default) is not slept through. The call
returns the failure instead, carrying retryAfterSeconds, so you can schedule around it. That
matters for the sleeping add-on containers: land and energy answer Retry-After: 60, and
parking your request handler for a minute inside one await is a worse surprise than an error
that tells you how long to wait.
Feedback submissions are never retried. POST /feedback creates a record and carries no
idempotency key, so a retry after a timeout risks filing the same report twice.
Retrying
Only 429, 502, and 503 are worth retrying, and only with backoff. A plain-text 503
basemap release is misconfigured is the exception: it has no Retry-After, and repeating it
does not restore the previous archive. A 429 already tells you
how long to wait via Retry-After, and a 429 with code: "billing_required" will not clear
until the payment method is fixed. A 503 with code: "service_unavailable" says
retryable: true in the body and carries a Retry-After sized to that service: 5 for geocode
and routing, 60 for land and energy, which are allowed to sleep. That is the one failure where
retrying is the right move, at the interval the header gives rather than a shorter one. A 400, 401, 403, 404, or 422 describes something about the
request or the account that a retry will not fix; fix the request instead.
Optimize
Unmap Optimize reports request errors in the response body, warnings in
response headers, and unserved shipments in skippedShipments. This section explains each status
and code and how to respond. Error messages identify field paths and indices without echoing
submitted coordinates or labels.
The error body
Errors on the Optimize paths use Google's error shape, so a client written for the Google Maps
Platform Route Optimization API reads them as it reads Google's own. The gateway's string code
sits beside error at the top level, as on every other Unmap endpoint:
// 400
{
"error": {
"code": 400,
"message": "The request has 1 validation error.",
"status": "INVALID_ARGUMENT",
"details": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{ "field": "model.shipments[3].pickups[0].timeWindows[1]", "description": "Time windows must be in increasing order and must not overlap or touch." }
]
}
]
},
"code": "bad_request"
}details holds one of these Google types:
google.rpc.BadRequestfor validation errors: onefieldViolationsentry per error,fieldthe exact path anddescriptionthe error message followed by its fix.google.rpc.PreconditionFailurefor mandatory shipments no vehicle can serve: oneviolationsentry per shipment (at most 100),typethe skip reason andsubjectthe shipment's path.google.rpc.RetryInfoon a retryable error, matching itsRetry-Afterheader, andgoogle.rpc.ErrorInfowith the reason on refusals from the key layer (a rate limit and a monthly quota are both429, and differ only there).
HTTP statuses
| HTTP | status | code | Cause | Fix |
|---|---|---|---|---|
400 | INVALID_ARGUMENT | bad_request | The body breaks the contract: see validation errors. The message counts the errors and says when the list stopped at maxValidationErrors. | Fix each field violation. Send solvingMode: VALIDATE_ONLY to get the full list without solving. |
400 | INVALID_ARGUMENT | bad_request | More shipments, vehicles or locations than the key's plan allows. The message names the limit. | Split the request. See limits per plan. |
400 | INVALID_ARGUMENT | bad_request | More travel matrix cells than the plan allows: 10,000 for dev, 250,000 for Indie or 1,000,000 for Growth, summed across vehicle matrices. | Split the request, merge duplicate stops, or use fewer distinct vehicle matrices. |
400 | INVALID_ARGUMENT | bad_request | More than 200 distinct locations need road travel, or the road travel would need more routing engine requests than one solve may make (usually because the stops span more than 1,000 km). | Split the request by region. |
400 | INVALID_ARGUMENT | bad_request | The costs are too large for the solver to scale to whole numbers, or the translated problem is too big to send to the solver. | Scale every cost down by one factor; split the request. |
400 | FAILED_PRECONDITION | bad_request | A mandatory shipment, with no penaltyCost and not ignored, cannot be served by any allowed vehicle. The message names the first one; a PreconditionFailure lists up to 100. Google may return 200 with mandatory shipments skipped; Unmap refuses instead. | Read the skip reason and fix the vehicle or shipment, or set penaltyCost to allow skipping. |
400 | FAILED_PRECONDITION | bad_request | The solver found no plan serving every mandatory shipment within its time budget. Passing the checks before solving does not guarantee that a shipment is feasible alone or with the others. | Add vehicles, widen windows and limits, make some shipments optional, or raise timeout. |
400 | FAILED_PRECONDITION | bad_request | No feasible plan exists even with every shipment skipped: the vehicles themselves are infeasible, for example a break that fits in no route. | Check the vehicles' time windows, breaks and limits. |
401 | UNAUTHENTICATED | unauthorized | No API key, or an unknown one. | Send a key; see Authentication. |
403 | PERMISSION_DENIED | origin_not_allowed | The key's allowed origins do not include this request's. | Add the origin to the key, or call from a server with an unscoped key. |
404 | NOT_FOUND | not_found | A method other than :optimizeTours under a Google path, or a method other than POST. | Use POST /optimize or POST /v1/projects/{project}:optimizeTours. |
413 | INVALID_ARGUMENT | payload_too_large | The body is over 4 MiB. | Split the request, or drop fields you do not need. |
429 | RESOURCE_EXHAUSTED | rate_limited | Every solve the plan allows at once is running, every optimizer instance is busy, or the key is over its rate limit. Retry-After says when to retry. | Wait and retry, or queue solves on your side. |
429 | RESOURCE_EXHAUSTED | rate_limited | Road travel is at capacity across the platform: other requests' road travel is being computed, and this request waited up to 20 seconds without a turn. The message says Road travel is at capacity right now. Nothing is billed. | Retry after Retry-After, or send useGeodesicDistances or your own durationDistanceMatrices, which never wait for road travel. |
429 | RESOURCE_EXHAUSTED | quota_exceeded, spend_cap, billing_required | The plan's monthly calls, the spend cap, or a failed payment. A solve whose shipments the remaining calls cannot cover is refused before solving. | See Plans & Limits. |
500 | INTERNAL | internal_error | An Unmap bug, never your fault. | Retry; report it if it repeats. |
503 | UNAVAILABLE | service_unavailable | The routing engine or the optimizer did not answer, for example while an optimizer instance starts. Retry-After says when to retry. | Retry after a short wait. |
504 | DEADLINE_EXCEEDED | deadline_exceeded | Road travel did not finish within its share of the request's 140 seconds, or the solver did not answer within the solve's time limit plus 20 seconds. | Split the request, or lower timeout. |
Body parsing, body size, authentication and shipment or vehicle count limits apply even with
VALIDATE_ONLY and can return non-200 responses. Validation checks fields and constraints; it
does not fetch road travel or prove that a feasible fleet plan exists.
The plan's limit on solves at once is checked only after a request has passed validation, and only
when it needs road travel or a solve. So an invalid request gets its 400 even while every allowed
solve is running, and VALIDATE_ONLY never waits.
A successful response never leaves out a mandatory shipment. Route polylines never change the
status: a route the engine cannot draw comes back without routePolyline, with a
ROUTE_POLYLINE_UNAVAILABLE warning.
Validation errors
Validation reports every problem it finds, up to maxValidationErrors (default 100, at most
10,000). With solvingMode: VALIDATE_ONLY they come back in a 200 body as validationErrors,
with the integer code below, the string code as displayName, a fields chain and
errorMessage; nothing is fetched or solved, and a valid request answers {}. In any other mode
they come back as the 400 above. The integers are Unmap's and never change; they do not match
Google's internal codes.
| Code | Integer | Cause | Fix |
|---|---|---|---|
INVALID_REQUEST | 1 | The body is not a JSON object. | Send a JSON object. |
UNKNOWN_FIELD | 2 | A field that neither the Google format nor Unmap defines, often a typo. | Check the name against field support. |
CONFLICTING_FIELD_NAMES | 3 | One field sent twice, in snake_case and in camelCase. | Send one spelling. |
INVALID_TYPE | 4 | A value of the wrong JSON type. | Send the type the field table lists. |
INVALID_ENUM_VALUE | 5 | An enum name or integer that does not exist, including SOLVING_MODE_UNSPECIFIED (the default is DEFAULT_SOLVE). Enum fields accept either the name or its proto integer. | Use a listed name or integer. |
INVALID_DURATION | 6 | A duration that is not a string such as "300s". | Send seconds with an s suffix. |
INVALID_TIMESTAMP | 7 | A timestamp that is not RFC 3339 or { seconds, nanos }. | Send RFC 3339, such as 2026-10-13T14:00:00Z. |
INVALID_INT64 | 8 | An integer that is not a decimal string or a safe JSON integer. | Send a decimal string. |
OUT_OF_RANGE | 9 | A number outside the field's range: a negative cost or duration, a latitude past 90, a geodesicMetersPerSecond under 1, a break minDuration that is not positive. Also a request with more than 16 distinct load type names over all loadDemands and loadLimits keys (the error names the first one past the limit), one load type's amounts over every pickup and delivery adding up past 2^53 - 1, a routeDistanceLimit.maxMeters over 1,000,000,000, or a globalEndTime more than 999,999,999 seconds (about 31 years) after globalStartTime. | Send a value in range; merge load types so at most 16 remain; scale amounts down; shorten the global window. |
INVALID_VALUE | 10 | A value the field cannot take: a duplicate or empty tag, a matrix without its own vehicleStartTag when there are several, a load type or visit type named after a built-in object property (__proto__, constructor, toString, prototype and the like), a soft limit at or above its hard limit, a soft load limit with a cost above 0 on a load type that both pickups and deliveries use, or any routeDistanceLimit.costPerKilometerBelowSoftMax, which GMPRO does not support either. | Make tags unique and not empty; tag every matrix when there are several; rename the type; keep a soft limit below its hard limit; put the soft load limit on a load type only pickups or only deliveries use; remove costPerKilometerBelowSoftMax. |
MISSING_REQUIRED_FIELD | 11 | A required field is absent: a shipment with no pickup or delivery, a visit with no location (or, with matrices, no tags), a waypoint with no location.latLng, a break with no start bounds, geodesicMetersPerSecond with useGeodesicDistances, matrix tags without matrices or the reverse, a soft time window, duration or distance cost without its soft bound or limit (a load cost alone is a soft max load of 0). | Add the field. |
MUTUALLY_EXCLUSIVE_FIELDS | 12 | Two fields that cannot both be set: arrivalLocation and arrivalWaypoint (or a vehicle's location and waypoint); any coordinates on a visit or vehicle when the model has durationDistanceMatrices; useGeodesicDistances with matrices. | Keep one. With matrices, place stops by tags only. |
INVALID_TIME_WINDOW | 13 | A window that is reversed, leaves the global window, or is out of order. Windows in one list must not overlap or touch; a vehicle's endTimeWindows must not close before its startTimeWindows opens; a break's earliest start must not follow its latest; globalEndTime must follow globalStartTime; a soft bound on a list with several windows, a softStartTime before startTime or a softEndTime after endTime, or a soft bound outside the global window. | Sort and separate the windows, keep them inside the global window, give the vehicle an end window it can reach, and keep a soft bound on a single window, with the soft start no earlier than startTime, the soft end no later than endTime, and both inside the global window. |
INVALID_REFERENCE | 14 | An index or tag that names nothing: an allowedVehicleIndices entry past the fleet, visit or vehicle tags that do not match exactly one source and one destination tag, or vehicle startTags that do not match exactly one vehicleStartTag once any matrix has one. | Point at something that exists, exactly once. |
MATRIX_SHAPE_MISMATCH | 15 | A matrix whose rows do not match durationDistanceMatrixSrcTags, or whose rows' durations or meters do not match durationDistanceMatrixDstTags. | One row per source tag, one cell per destination tag. |
FIELD_NOT_SUPPORTED_YET | 16 | A field Unmap plans to support, set to a value other than its default. Field support gives each one's exact message. | Remove the field or leave it at its default. |
VALUE_NOT_SUPPORTED_YET | 17 | A value Unmap plans to support: solvingMode DETECT_SOME_INFEASIBLE_SHIPMENTS or TRANSFORM_AND_RETURN_REQUEST, travelMode WALKING, unmapProfile truck or pedestrian, more than one pickup or delivery per shipment, more than one vehicle start or end time window. | Use a supported value; split alternatives into separate requests. |
PLACE_ID_NOT_SUPPORTED | 18 | A waypoint given as a Google place ID. | Geocode with /geocode/search and send coordinates. |
GCS_URI_NOT_SUPPORTED | 19 | A gs:// Google Cloud Storage URI. No field of optimizeTours takes a URI; the code is reserved for the batch methods, which are not available yet. | Send the body inline. |
Warnings
A field Unmap accepts but cannot honour produces a FIELD_IGNORED warning when it is set to a
value other than its default: one per field, with the first path where it appeared and a count.
Warnings never go in the response body, which stays the shape a Google client expects: they ride
the X-Unmap-Optimize-Warnings response header, a JSON array, and come back with
VALIDATE_ONLY too. The fix is the same for each: remove the field, or accept that it has no
effect.
| Field | Warning |
|---|---|
parent | parent is ignored: Unmap has no projects, so the request runs under the API key's account. |
considerRoadTraffic | considerRoadTraffic is ignored: Unmap travel times do not use live or historical traffic yet. |
populateTransitionPolylines | populateTransitionPolylines is ignored: Unmap returns one polyline per route, as routes[].routePolyline when populatePolylines is true. |
allowLargeDeadlineDespiteInterruptionRisk | allowLargeDeadlineDespiteInterruptionRisk is ignored: Unmap caps the timeout per plan and clamps anything longer. |
any waypoint's location.heading | location.heading is ignored: Unmap snaps a location to the nearest road whatever the direction of travel. |
any waypoint's sideOfRoad | sideOfRoad is ignored: Unmap travel times do not model which side of the road a stop is on. |
any waypoint's vehicleStopover | vehicleStopover is ignored: Unmap snaps every stop to the nearest road a vehicle can use. |
The same header carries the gateway's own warnings:
| Code | Meaning |
|---|---|
TIMEOUT_CLAMPED | timeout was over the plan's longest solve and was shortened. A Warning header says the same. |
ROAD_TRAVEL_DEGRADED | Some road travel cells were set unreachable because a per-request routing budget ran out, not because no road was found. A shipment skipped as unreachable may be servable: split the request by region. |
ROAD_TRAVEL_UNCONFIRMED | Some cells the routing engine left empty were set unreachable without the usual confirming route request. Split the request into smaller ones. |
ROUTE_POLYLINE_UNAVAILABLE | The polyline could not be drawn for the vehicles named. Their routes are complete, without routePolyline. |
WARNINGS_TRUNCATED | The header reached its 4 KB limit; count says how many warnings were left out. |
Skip reasons
A shipment Unmap does not serve is listed in skippedShipments with a reason code from Google's
published SkippedShipment.Reason.Code list. A skipped optional shipment adds its penaltyCost to
metrics.totalCost; an ignored one costs nothing. A skipped shipment is never billed.
Before solving, a shipment is skipped only when no plan can serve it: every vehicle it may use is
ruled out with the shipment alone on an empty route. Such a shipment never reaches the solver. If
it is mandatory, the request fails instead (400 FAILED_PRECONDITION, above). Each reason names
the first vehicle it applies to (exampleVehicleIndex). After solving, the shipments the solver
left out are checked again, against each vehicle's own route, and listed with the reasons that
rule every vehicle out.
These checks are conservative about travel. Road travel and supplied matrices need not obey the triangle inequality (a stop can be quicker to reach through another stop than directly), so the checks before solving use a lower bound that holds for any matrix, and breaks and waiting are left out. That is why a shipment can pass them and still be left out by the solver.
| Code | When | Cause | Fix |
|---|---|---|---|
SHIPMENT_IGNORED | before | The shipment has ignore: true. No penalty, not billed. | Set ignore to false to plan it. |
NO_VEHICLE | before | The model has no vehicles. | Add a vehicle. |
DEMAND_EXCEEDS_VEHICLE_CAPACITY | both | The shipment's demand for exampleExceededCapacityType is more than the vehicle's maxLoad; after solving, more than the room left on its route. | Raise loadLimits, split the shipment, or allow a bigger vehicle. |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TIME_WINDOWS | both | Starting at its earliest time, the vehicle cannot reach a visit inside one of its windows, or cannot finish and reach its end in time. Also used when a stop cannot be reached at all, for example an island with no road: Google's list has no code for an unreachable stop. | Widen the visit or vehicle windows, start earlier, or check the stop's location. |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DURATION_LIMIT | both | Travel plus service time is more than routeDurationLimit.maxDuration. | Raise the limit or use another vehicle. |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TRAVEL_DURATION_LIMIT | both | Travel is more than travelDurationLimit.maxDuration. | Raise the limit or use another vehicle. |
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DISTANCE_LIMIT | both | Distance is more than routeDistanceLimit.maxMeters. | Raise the limit or use another vehicle. |
ZERO_PENALTY_COST | after | Nothing hard stopped the shipment and its penaltyCost is 0, so skipping it was free. | Give it a penalty that reflects what missing it costs you. |
no reasons | after | Nothing the checks can see stopped it: serving it would cost more than its penaltyCost, or it conflicts with other shipments in ways a single-vehicle check does not model (windows along a busy route, breaks). Unmap never sends CODE_UNSPECIFIED. | Raise its penaltyCost, add capacity, or widen windows. |
Codes Unmap never reports: CODE_UNSPECIFIED (Google's list says it is never used),
VEHICLE_NOT_ALLOWED (Unmap reports why the allowed vehicles fail instead), VEHICLE_IGNORED
(Vehicle.ignore is not supported yet), and SKIPPED_IN_INJECTED_SOLUTION_CONSTRAINT and
VEHICLE_ROUTE_IS_FULLY_SEQUENCE_CONSTRAINED (injected solutions are not supported yet).