Skip to content

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 plan

Everything 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. Code unauthorized. Every data-plane 401 also carries an RFC 6750 WWW-Authenticate challenge 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", with error="invalid_token" added when a key was presented but not recognised.
  • 429: one of four things, told apart by code:
    • 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. No Retry-After, because no amount of waiting fixes it; update the payment method in the dashboard.
  • 403: one of four things, told apart by code:
    • origin_not_allowed: the key carries a non-empty allowed-origins list and this request is not covered by it. A missing Origin header, 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 carries capability (legalLand, energy, agriculture, remote, mining, or ski), addon naming the cheapest SKU that would grant it (addon-legal-land, addon-energy, addon-agriculture, addon-remote, addon-mining, addon-ski), and a message written for a person where error is 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 from localhost, 127.0.0.1, *.localhost, *.pages.dev and *.workers.dev, where every capability is free. It carries the same capability, addon and message fields, 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 same capability, addon and message fields.
  • 400: a malformed request parameter, code bad_request unless a more specific one applies. Bad tile coordinates, a missing q on a geocoding call, a malformed bbox, a missing or non-numeric near, a longitude or latitude outside WGS84 on geocoding or places, an unknown category, both or neither of near and bbox on a category search, more than four isochrone contours or one over 120 minutes, an invalid theme code, an unknown /data/query layer, and so on. The message string names the parameter.
  • 501: GET /data/query on a catalogued tile-only layer (no PostGIS table). Code not_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 configured when 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 use not_found, and so does an unknown resource on the account API. A missing key is 401 before 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 on code.
    • outside_coverage: an anchor is outside Canada. The body is { "error": "outside Canadian coverage", "code": "outside_coverage" }. A mode=transit isochrone whose centre has no scheduled transit nearby uses the same code with the error no 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/nearby and /places/nearby all use it. A text search or autocomplete with no match stays an empty list. A longitude or latitude outside WGS84 is 400 bad_request on geocoding and places, and 400 invalid_location on routing. It is not outside_coverage.
    • no_route: the point is inside Canada and the graph still has no answer. On /route the 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, are no_route. /match uses no_route when a Canadian trace cannot be snapped to any road. /matrix keeps a pair the engine merely fails to connect as a null cell inside a 200, with the rest of the grid intact, so handle null on every cell. It answers 422 no_route when 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 is outside_coverage before that check. /optimized-route fails whole for those same graph reasons, confirmed against the deployed gateway, though a null in its internal matrix alone does not fail it; see Routing.
  • 502: an upstream answered with a failure of its own. Code upstream_error: the routing container failed, or /elevation met 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, a retryable flag, and a Retry-After:

      { "error": "geocode temporarily unavailable", "code": "service_unavailable", "retryable": true }

      The service name is geocode, routing, land or energy. Nothing about why is disclosed, deliberately: a container's own error text can name an image tag or a database message.

      Retry-After differs by service, and the difference is not cosmetic. geocode and routing are kept warm by a cron that pings them every five minutes, so their 503 is a blip and the header says 5. land and energy, 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 says 60. Honour the header. Retrying a land 503 every 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 of retryable, 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 misconfigured

      Cache-Control is private, 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 is 404 basemap release not found, also private, no-store. An id that is not eight digits is that same 404 body 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 container

A 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.BadRequest for validation errors: one fieldViolations entry per error, field the exact path and description the error message followed by its fix.
  • google.rpc.PreconditionFailure for mandatory shipments no vehicle can serve: one violations entry per shipment (at most 100), type the skip reason and subject the shipment's path.
  • google.rpc.RetryInfo on a retryable error, matching its Retry-After header, and google.rpc.ErrorInfo with the reason on refusals from the key layer (a rate limit and a monthly quota are both 429, and differ only there).

HTTP statuses

HTTPstatuscodeCauseFix
400INVALID_ARGUMENTbad_requestThe 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.
400INVALID_ARGUMENTbad_requestMore shipments, vehicles or locations than the key's plan allows. The message names the limit.Split the request. See limits per plan.
400INVALID_ARGUMENTbad_requestMore 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.
400INVALID_ARGUMENTbad_requestMore 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.
400INVALID_ARGUMENTbad_requestThe 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.
400FAILED_PRECONDITIONbad_requestA 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.
400FAILED_PRECONDITIONbad_requestThe 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.
400FAILED_PRECONDITIONbad_requestNo 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.
401UNAUTHENTICATEDunauthorizedNo API key, or an unknown one.Send a key; see Authentication.
403PERMISSION_DENIEDorigin_not_allowedThe 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.
404NOT_FOUNDnot_foundA method other than :optimizeTours under a Google path, or a method other than POST.Use POST /optimize or POST /v1/projects/{project}:optimizeTours.
413INVALID_ARGUMENTpayload_too_largeThe body is over 4 MiB.Split the request, or drop fields you do not need.
429RESOURCE_EXHAUSTEDrate_limitedEvery 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.
429RESOURCE_EXHAUSTEDrate_limitedRoad 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.
429RESOURCE_EXHAUSTEDquota_exceeded, spend_cap, billing_requiredThe 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.
500INTERNALinternal_errorAn Unmap bug, never your fault.Retry; report it if it repeats.
503UNAVAILABLEservice_unavailableThe 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.
504DEADLINE_EXCEEDEDdeadline_exceededRoad 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.

CodeIntegerCauseFix
INVALID_REQUEST1The body is not a JSON object.Send a JSON object.
UNKNOWN_FIELD2A field that neither the Google format nor Unmap defines, often a typo.Check the name against field support.
CONFLICTING_FIELD_NAMES3One field sent twice, in snake_case and in camelCase.Send one spelling.
INVALID_TYPE4A value of the wrong JSON type.Send the type the field table lists.
INVALID_ENUM_VALUE5An 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_DURATION6A duration that is not a string such as "300s".Send seconds with an s suffix.
INVALID_TIMESTAMP7A timestamp that is not RFC 3339 or { seconds, nanos }.Send RFC 3339, such as 2026-10-13T14:00:00Z.
INVALID_INT648An integer that is not a decimal string or a safe JSON integer.Send a decimal string.
OUT_OF_RANGE9A 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_VALUE10A 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_FIELD11A 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_FIELDS12Two 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_WINDOW13A 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_REFERENCE14An 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_MISMATCH15A 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_YET16A 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_YET17A 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_SUPPORTED18A waypoint given as a Google place ID.Geocode with /geocode/search and send coordinates.
GCS_URI_NOT_SUPPORTED19A 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.

FieldWarning
parentparent is ignored: Unmap has no projects, so the request runs under the API key's account.
considerRoadTrafficconsiderRoadTraffic is ignored: Unmap travel times do not use live or historical traffic yet.
populateTransitionPolylinespopulateTransitionPolylines is ignored: Unmap returns one polyline per route, as routes[].routePolyline when populatePolylines is true.
allowLargeDeadlineDespiteInterruptionRiskallowLargeDeadlineDespiteInterruptionRisk is ignored: Unmap caps the timeout per plan and clamps anything longer.
any waypoint's location.headinglocation.heading is ignored: Unmap snaps a location to the nearest road whatever the direction of travel.
any waypoint's sideOfRoadsideOfRoad is ignored: Unmap travel times do not model which side of the road a stop is on.
any waypoint's vehicleStopovervehicleStopover is ignored: Unmap snaps every stop to the nearest road a vehicle can use.

The same header carries the gateway's own warnings:

CodeMeaning
TIMEOUT_CLAMPEDtimeout was over the plan's longest solve and was shortened. A Warning header says the same.
ROAD_TRAVEL_DEGRADEDSome 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_UNCONFIRMEDSome cells the routing engine left empty were set unreachable without the usual confirming route request. Split the request into smaller ones.
ROUTE_POLYLINE_UNAVAILABLEThe polyline could not be drawn for the vehicles named. Their routes are complete, without routePolyline.
WARNINGS_TRUNCATEDThe 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.

CodeWhenCauseFix
SHIPMENT_IGNOREDbeforeThe shipment has ignore: true. No penalty, not billed.Set ignore to false to plan it.
NO_VEHICLEbeforeThe model has no vehicles.Add a vehicle.
DEMAND_EXCEEDS_VEHICLE_CAPACITYbothThe 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_WINDOWSbothStarting 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_LIMITbothTravel plus service time is more than routeDurationLimit.maxDuration.Raise the limit or use another vehicle.
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_TRAVEL_DURATION_LIMITbothTravel is more than travelDurationLimit.maxDuration.Raise the limit or use another vehicle.
CANNOT_BE_PERFORMED_WITHIN_VEHICLE_DISTANCE_LIMITbothDistance is more than routeDistanceLimit.maxMeters.Raise the limit or use another vehicle.
ZERO_PENALTY_COSTafterNothing 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 reasonsafterNothing 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).