Errors
Every Mountaya API describes a failure the same way: a top-level errors array whose entries carry a human-readable message and a machine-readable extensions.code. The code vocabulary is closed, so a client branches on the code rather than matching on message text.
The two surfaces differ in where they put the failure, not in how they describe it. The Map Studio API sets the HTTP status and answers with the envelope. The Data API is GraphQL, so a field-level failure arrives under 200 OK with the code in that same envelope.
The envelope
messagestring
A description of the failure written for a person, localized by the Accept-Language header. Read extensions.code to branch; message text is not a contract.
patharray
The field the entry refers to. Present on field-level entries, absent otherwise. The two surfaces prefix it differently, and the Data API puts it in a second place as well; see where the field path lives.
extensions.codestring
One of the sixteen codes below. Always present.
One entry is emitted per failed validation, so a request with three rejected fields answers with three entries rather than one.
The codes
Fourteen codes name an HTTP status. Two do not, and the table shows the status they ride on in parentheses. VALIDATION_FAILED replaces BAD_REQUEST on the per-field entries of a 400, so a client can tell a field-specific failure from a malformed request without parsing messages. CARD_REQUIRED narrows a 402 to the case adding a card fixes; see payment and card errors.
Every 5xx response is opaque by construction: one entry, a generic localized message, no field path, and no metadata. Nothing about the internal failure reaches a client, so there is nothing in a 5xx body to branch on beyond the code itself.
Where the field path lives
Both surfaces name the offending field, in the position that is idiomatic for each.
The Map Studio API puts it on the entry's own path, prefixed by where in the request the field sits:
["request", "body", …]for a body field.["request", "params", …]for a path parameter.["request", "query", …]for a query parameter.
The Data API's entry-level path is already spoken for: GraphQL uses it for the response path, naming which field of your query failed. The input field path is mirrored onto extensions.path instead, prefixed with ["input", …]:
How each surface answers
Map Studio API
The HTTP status carries the category and the body carries the detail. Every public operation declares 401, 500, and 504; most also declare 400, 403, and 404. Read the status first, then the codes in errors.
A successful Map Studio API call answers with data instead, and response-scoped metadata rides under extensions.metadata:
Data API
A GraphQL query that reaches the resolvers answers with 200 OK whatever happens next, so the status tells you almost nothing. Inspect errors on every response. A partial result returns both data and errors together.
Two classes of failure still answer with a 4xx, because they are rejected before any resolver runs:
- A malformed query: a document that does not parse, or that asks for a field the schema does not declare.
- A credential or quota failure: a missing key, an expired session token, or a spent rate limit.
Payment and card errors
Two different problems answer with 402 Payment Required, and they call for opposite responses from a client. The code tells them apart.
PAYMENT_REQUIRED402
The organization does not hold the product this call needs. Retrying will not help, and neither will a card: someone has to enable the product for the organization.
CARD_REQUIRED402
The organization holds the product but has no payment method on file. The action succeeds once a card is added. This code is Map Studio API-only.
CARD_REQUIRED is a per-entry refinement of a 402 rather than a status of its own, and it is absent from the Data API's published vocabulary. The gate that raises it there reports the status it belongs to, so the same condition arrives as PAYMENT_REQUIRED. Branch on CARD_REQUIRED only where you are calling the Map Studio API, and treat a plain PAYMENT_REQUIRED as covering both cases everywhere else.
Timeouts
Every Map Studio API route runs under a request time budget. When the budget is spent, the work in flight fails wherever it happened to be, and the response is restated as 504 Gateway Timeout with GATEWAY_TIMEOUT — otherwise you would receive whichever internal error the interruption happened to produce, which says nothing useful about what went wrong.
The restatement only ever replaces a 5xx. A request refused on its own merits was answered before the budget mattered, so an expiring deadline never turns a 404 into a 504.
All 50 public Map Studio API operations declare 504. A 504 is worth retrying: the budget is per request, so the same call may well fit next time. Retry it once, then treat repeated timeouts as a signal that the request is too large — fewer waypoints, a smaller bounding box, a narrower page.
Rate limits
Exceeding a limit answers with 429 Too Many Requests and TOO_MANY_REQUESTS. No Retry-After header accompanies it, so back off on your own schedule: retry after a couple of seconds, widen the gap if it happens again, and wait out the remainder of the minute if a large burst exhausted the window.
Which limit you hit changes whether backing off helps. The Tile and Data API quotas are per organization and refill every minute, so a retry succeeds once the window rolls over. Session creation is different: its per-key counter charges only failed validations, so a 429 there that survives a few retries means the secret key itself is rejected, not that you are going too fast.
See authentication for the limits themselves and how they are counted.
Conflicts
A write that collides answers with 409 Conflict and names what it collided with. Both surfaces report the same fact, in the position each envelope reserves for it: the Map Studio API puts it at the response level, a sibling of errors, in the shape that operation's ResponseMetadata* schema declares, while the Data API attaches it per error at errors[].extensions.metadata, because the GraphQL envelope hangs per-error detail on the error object.
Two collisions carry metadata, and they are not the same failure.
A name already taken
Four itinerary writes can collide on a name, across two schemas that differ only in the operation they belong to. ResponseMetadataPostItinerary covers the three creates — POST /v1/itineraries, POST /v1/itineraries/import and POST /v1/itineraries/{itinerary_id}/duplicate — and ResponseMetadataPatchItinerary covers the rename, PATCH /v1/itineraries/{itinerary_id}. Both put the conflicting itinerary's id under itinerary, so one branch reads all four:
Collections collide on names the same way, and the same create/rename split applies: POST /v1/collections uses ResponseMetadataPostCollection and PATCH /v1/collections/{collection_id} uses ResponseMetadataPatchCollection. The metadata key follows the resource rather than the operation — it is named for what you were writing and holds that resource's id — so both put the conflicting collection's id under collection:
A client that already handles the itinerary case handles this one by reading a different key, not a different envelope.
A collaborator holds the lock
Editing an itinerary takes a soft lock on the part being changed, and a write that would change a part somebody else holds is refused. Those refusals answer with ResponseMetadataLockConflict, which names the holder at extensions.metadata.holder.id:
One shape covers every write in the editing session, so a client branches once on "somebody else is editing this" rather than once per endpoint. Eleven public operations declare it:
POST /v1/itineraries/{itinerary_id}/segments/insertPUT /v1/itineraries/{itinerary_id}/segments/{segment_id}DELETE /v1/itineraries/{itinerary_id}/segments/{segment_id}POST /v1/itineraries/{itinerary_id}/segments/{segment_id}/mergePOST /v1/itineraries/{itinerary_id}/segments/{segment_id}/splitPATCH /v1/itineraries/{itinerary_id}/segments/{segment_id}/modePUT /v1/itineraries/{itinerary_id}/segment-groups/{group_id}DELETE /v1/itineraries/{itinerary_id}/segment-groups/{group_id}PUT /v1/itineraries/{itinerary_id}/waypoints/{waypoint_id}DELETE /v1/itineraries/{itinerary_id}/waypoints/{waypoint_id}POST /v1/itineraries/{itinerary_id}/publish
A holder.id of 00000000-0000-0000-0000-000000000000 means nobody holds the entity at all: the write needed a lock the caller had not taken. It is the same refusal from the server's side and a different fix from yours — take the lock rather than wait for someone to release it.