{
  "openapi": "3.1.0",
  "info": {
    "title": "VIETMAP Maps API",
    "version": "4.0",
    "summary": "Map APIs for Vietnam — address search, geocoding, routing, distance matrix and route optimization.",
    "description": "Map APIs built on VIETMAP's own nationwide map data for Vietnam: 600,000+ km of roads,\n18 million+ addresses, 9.3 million+ places, refreshed weekly.\n\nThis document covers eleven endpoints across search, routing, matrix and map display. Every request authenticates with an\n`apikey` query parameter — use your **services key**, not the tilemap key.\nGet a key at https://maps.vietmap.vn/console-v2/register (60,000 free transactions,\nno credit card).\n\n**Coordinates** in query parameters are `lat,lng` — latitude first. Coordinates inside JSON\nbodies (Map Matching) are `[longitude, latitude]`.\n\n**Errors** are signalled by HTTP status: `401` for a missing or wrong key (empty body),\n`423` when the key is out of quota or the API is not enabled for it (plain-text body).\nRouting-family endpoints also return a `code` field in the JSON body.\n\n**Not in this spec yet:** VRP (fleet routing), Isochrone and Geofencing. Try VRP on the\nPlayground at https://tools.vietmap.vn/playground/vrp; for Isochrone and Geofencing, or\nanything else this document does not answer, contact support on Zalo at\nhttps://zalo.me/3189066936017422854 or email maps.info@vietmap.vn.\n\nFull HTML documentation: https://maps.vietmap.vn/docs\nLLM-oriented guides: https://maps.vietmap.vn/docs/llms.txt\n",
    "contact": {
      "name": "VIETMAP Maps API",
      "url": "https://maps.vietmap.vn",
      "email": "maps.info@vietmap.vn"
    },
    "termsOfService": "https://maps.vietmap.vn/console/privacy-policy"
  },
  "externalDocs": {
    "description": "VIETMAP Maps API documentation",
    "url": "https://maps.vietmap.vn/docs"
  },
  "servers": [
    {
      "url": "https://maps.vietmap.vn",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apikey": []
    }
  ],
  "tags": [
    {
      "name": "Search",
      "description": "Autocomplete, geocoding and place details. Results carry a `ref_id` that Place v4 resolves to coordinates."
    },
    {
      "name": "Routing",
      "description": "Turn-by-turn routing and single-vehicle stop ordering. Both return the same `paths` structure."
    },
    {
      "name": "Matrix",
      "description": "Travel time and distance between many origins and many destinations."
    },
    {
      "name": "Map display",
      "description": "Vector styles, raster tiles and rendered map images."
    }
  ],
  "paths": {
    "/api/autocomplete/v4": {
      "get": {
        "tags": [
          "Search"
        ],
        "operationId": "autocomplete",
        "summary": "Suggest places while the user types",
        "description": "Returns up to 10 suggestions for a partial address or place name. Send at least\ntwo characters. Pass `focus` to rank nearby results first. The response has no\ncoordinates — send `ref_id` to `/api/place/v4` after the user picks a suggestion.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/text"
          },
          {
            "$ref": "#/components/parameters/focus"
          },
          {
            "$ref": "#/components/parameters/display_type"
          },
          {
            "$ref": "#/components/parameters/layers"
          },
          {
            "$ref": "#/components/parameters/cityId"
          },
          {
            "$ref": "#/components/parameters/distId"
          },
          {
            "$ref": "#/components/parameters/wardId"
          },
          {
            "$ref": "#/components/parameters/circle_center"
          },
          {
            "$ref": "#/components/parameters/circle_radius"
          },
          {
            "$ref": "#/components/parameters/cats"
          },
          {
            "$ref": "#/components/parameters/admin_new"
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions, best match first. Empty array when nothing matches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SearchResult"
                  }
                },
                "example": [
                  {
                    "ref_id": "geocode:RAkPcicmZ3d-NQhac2kADHYlbFAkBiEeAQAkCV0EXwdF",
                    "distance": 0.0691,
                    "address": "Phường 4,Quận 5,Thành Phố Hồ Chí Minh",
                    "name": "197 Trần Phú",
                    "display": "197 Trần Phú Phường 4,Quận 5,Thành Phố Hồ Chí Minh",
                    "boundaries": [
                      {
                        "type": 2,
                        "id": 656652,
                        "name": "4",
                        "prefix": "Phường",
                        "full_name": "Phường 4"
                      },
                      {
                        "type": 1,
                        "id": 1292,
                        "name": "5",
                        "prefix": "Quận",
                        "full_name": "Quận 5"
                      },
                      {
                        "type": 0,
                        "id": 12,
                        "name": "Hồ Chí Minh",
                        "prefix": "Thành Phố",
                        "full_name": "Thành Phố Hồ Chí Minh"
                      }
                    ],
                    "categories": [],
                    "entry_points": [],
                    "data_old": null,
                    "data_new": null
                  }
                ]
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/search/v4": {
      "get": {
        "tags": [
          "Search"
        ],
        "operationId": "geocode",
        "summary": "Geocode a full address string",
        "description": "Resolves a complete address (house number, street, ward, province) to candidate\nplaces. For suggestions while typing use `/api/autocomplete/v4` instead. Like\nAutocomplete, the response has no coordinates — resolve `ref_id` with `/api/place/v4`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/text"
          },
          {
            "$ref": "#/components/parameters/focus"
          },
          {
            "$ref": "#/components/parameters/display_type"
          },
          {
            "$ref": "#/components/parameters/layers"
          },
          {
            "$ref": "#/components/parameters/cityId"
          },
          {
            "$ref": "#/components/parameters/distId"
          },
          {
            "$ref": "#/components/parameters/wardId"
          },
          {
            "$ref": "#/components/parameters/circle_center"
          },
          {
            "$ref": "#/components/parameters/circle_radius"
          },
          {
            "$ref": "#/components/parameters/cats"
          },
          {
            "$ref": "#/components/parameters/admin_new"
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate places, best match first. Empty array when nothing matches.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SearchResult"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/place/v4": {
      "get": {
        "tags": [
          "Search"
        ],
        "operationId": "placeDetail",
        "summary": "Resolve a ref_id to coordinates and address components",
        "description": "Takes the `ref_id` returned by Autocomplete v4 or Geocode v4 and returns the\nplace's coordinates and structured address. Keep the prefix (`auto:` or `geocode:`)\n— it decides the response format. Call this from your backend so the API key\nstays off the client.\n",
        "parameters": [
          {
            "name": "refid",
            "in": "query",
            "required": true,
            "description": "The `ref_id` exactly as returned by Autocomplete v4 or Geocode v4, prefix included.",
            "schema": {
              "type": "string"
            },
            "example": "auto:RAkPcicmZ3d-NQhac2kADHYlbFAkBiEeAQAkCV0EXwdF"
          }
        ],
        "responses": {
          "200": {
            "description": "Place details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlaceDetail"
                },
                "example": {
                  "display": "197 Đường Trần Phú,Phường Chợ Quán,Thành Phố Hồ Chí Minh",
                  "name": "",
                  "hs_num": "197",
                  "street": "Đường Trần Phú",
                  "address": "197 Đường Trần Phú",
                  "city_id": 12,
                  "city": "Thành Phố Hồ Chí Minh",
                  "district_id": 0,
                  "district": "",
                  "ward_id": 18700,
                  "ward": "Phường Chợ Quán",
                  "lat": 10.759222947000069,
                  "lng": 106.67590269100003
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/route/v4": {
      "get": {
        "tags": [
          "Routing"
        ],
        "operationId": "route",
        "summary": "Route between two or more points",
        "description": "Turn-by-turn route for car, motorcycle, truck or container. Truck and container\nprofiles honour weight limits and restricted roads. Add `annotations=toll` to get\nthe total toll cost and a per-station breakdown for the chosen vehicle.\n\nBilling: `⌊number of points / 5⌋ + 1` transactions per request.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/point"
          },
          {
            "$ref": "#/components/parameters/points_encoded"
          },
          {
            "name": "vehicle",
            "in": "query",
            "description": "Vehicle profile. Decides speeds and which roads are allowed.",
            "schema": {
              "type": "string",
              "enum": [
                "car",
                "motorcycle",
                "truck",
                "container"
              ],
              "default": "car"
            }
          },
          {
            "name": "capacity",
            "in": "query",
            "description": "Gross vehicle weight in kilograms. Required when `vehicle=truck`.",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 2000
          },
          {
            "name": "avoid",
            "in": "query",
            "description": "Road types to avoid.",
            "schema": {
              "type": "string",
              "enum": [
                "ferry"
              ]
            }
          },
          {
            "name": "time",
            "in": "query",
            "description": "Departure time, ISO-8601 in UTC. Defaults to now.",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2025-08-01T12:01:00Z"
          },
          {
            "name": "alternative",
            "in": "query",
            "description": "Set `true` to also return alternative routes.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "heading",
            "in": "query",
            "description": "Current heading of the vehicle in degrees, `0` = north. Used to pick the correct departure direction.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 360
            }
          },
          {
            "name": "annotations",
            "in": "query",
            "description": "Extra data to include, comma-separated. `toll` fills `toll_cost` and `tolls`\non each path. Requesting annotations increases response time and size.\n",
            "schema": {
              "type": "string"
            },
            "example": "congestion,toll"
          }
        ],
        "responses": {
          "200": {
            "description": "One or more routes. Check `code` — `ZERO_RESULTS` means no route exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RouteResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/matrix/v4": {
      "get": {
        "tags": [
          "Matrix"
        ],
        "operationId": "matrix",
        "summary": "Travel time and distance between many points",
        "description": "Returns an n × m matrix of durations and distances. Pass every point once with the\nrepeated `point` parameter, then select which indexes are origins (`sources`) and\nwhich are destinations (`destinations`). Indexes count from `0` in the order sent.\n\nBilling: `n × m` transactions per request.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/point"
          },
          {
            "name": "vehicle",
            "in": "query",
            "description": "Vehicle profile used to compute the matrix.",
            "schema": {
              "type": "string",
              "enum": [
                "car",
                "motorcycle",
                "truck",
                "container"
              ],
              "default": "car"
            }
          },
          {
            "name": "sources",
            "in": "query",
            "description": "Indexes of the origin points, separated by `;`, or `all`.",
            "schema": {
              "type": "string",
              "default": "all"
            },
            "example": "0;1"
          },
          {
            "name": "destinations",
            "in": "query",
            "description": "Indexes of the destination points, separated by `;`, or `all`.",
            "schema": {
              "type": "string",
              "default": "all"
            },
            "example": "2;3;4;5"
          },
          {
            "name": "annotation",
            "in": "query",
            "description": "Which values to compute. `duration` in seconds, `distance` in meters.",
            "schema": {
              "type": "string",
              "enum": [
                "duration",
                "distance"
              ],
              "default": "duration"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matrix rows follow `sources` order, columns follow `destinations` order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatrixResponse"
                },
                "example": {
                  "code": "OK",
                  "messages": null,
                  "durations": [
                    [
                      230,
                      201,
                      386,
                      193
                    ],
                    [
                      239,
                      209,
                      394,
                      201
                    ]
                  ],
                  "distances": [
                    [
                      1766.3,
                      1374.3,
                      1952.2,
                      1113.5
                    ],
                    [
                      1803.4,
                      1411.4,
                      1989.3,
                      1150.6
                    ]
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/tsp/v4": {
      "get": {
        "tags": [
          "Routing"
        ],
        "operationId": "tsp",
        "summary": "Order stops for a single vehicle",
        "description": "Traveling-salesman optimization: returns the route for one vehicle through every\npoint with the lowest total driving time. By default the route returns to the first\npoint; change that with `roundtrip`, `sources` and `destinations`. For a fleet of\nvehicles use the VRP endpoint.\n\nBilling: one transaction per point.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/point"
          },
          {
            "$ref": "#/components/parameters/points_encoded"
          },
          {
            "name": "vehicle",
            "in": "query",
            "description": "Vehicle profile.",
            "schema": {
              "type": "string",
              "enum": [
                "car",
                "motorcycle",
                "truck",
                "container"
              ],
              "default": "car"
            }
          },
          {
            "name": "roundtrip",
            "in": "query",
            "description": "`true` returns to the first point after the last stop.",
            "schema": {
              "type": "boolean",
              "default": true
            }
          },
          {
            "name": "sources",
            "in": "query",
            "description": "`first` starts the tour at the first point sent; `any` lets the solver choose.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "first"
              ],
              "default": "any"
            }
          },
          {
            "name": "destinations",
            "in": "query",
            "description": "`last` ends the tour at the last point sent; `any` lets the solver choose.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "last"
              ],
              "default": "any"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The optimized tour as a single path.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TspResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/reverse/v4": {
      "get": {
        "tags": [
          "Search"
        ],
        "operationId": "reverseGeocode",
        "summary": "Nearest address for a coordinate",
        "description": "Returns the closest known address or place for a `lat`/`lng` pair. Unlike\nAutocomplete and Geocode, the results already carry coordinates. With\n`display_type=4` the list holds two elements — one per address format.\n",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude. Roughly 8–23 for Vietnam.",
            "schema": {
              "type": "number",
              "format": "double"
            },
            "example": 10.759221
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "description": "Longitude. Roughly 102–110 for Vietnam.",
            "schema": {
              "type": "number",
              "format": "double"
            },
            "example": 106.675901
          },
          {
            "name": "display_type",
            "in": "query",
            "description": "Address format: `1` new two-tier, `2` old three-tier, `4` two list elements (new then old),\n`5` new format with the old one in `data_old`, `6` old format with the new one in `data_new`.\n",
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2,
                4,
                5,
                6
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching places, nearest first. Empty array when nothing is near enough.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReverseResult"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/migrate-address/v3": {
      "get": {
        "tags": [
          "Search"
        ],
        "operationId": "migrateAddress",
        "summary": "Convert one address between old and new administrative units",
        "description": "Rewrites a free-text address from the pre-2025 three-tier format to the post-merger\ntwo-tier format, or back. One address per call — loop over your records to migrate\na database. The call limit is not restricted for this endpoint.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/text"
          },
          {
            "name": "focus",
            "in": "query",
            "description": "A nearby `lat,lng` used to disambiguate duplicate names. **Required when `migrate_type=2`.**",
            "schema": {
              "type": "string",
              "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"
            },
            "example": "10.758867,106.675566"
          },
          {
            "name": "migrate_type",
            "in": "query",
            "description": "Direction: `1` old → new, `2` new → old (needs `focus`).",
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2
              ],
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The converted address.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MigrateResult"
                },
                "example": {
                  "address": "Phường Chợ Quán,Thành Phố Hồ Chí Minh",
                  "name": "197 tran phu ",
                  "display": "197 tran phu Phường Chợ Quán,Thành Phố Hồ Chí Minh",
                  "boundaries": [
                    {
                      "type": 2,
                      "id": 18700,
                      "name": "Chợ Quán",
                      "prefix": "Phường",
                      "full_name": "Phường Chợ Quán"
                    },
                    {
                      "type": 0,
                      "id": 12,
                      "name": "Hồ Chí Minh",
                      "prefix": "Thành Phố",
                      "full_name": "Thành Phố Hồ Chí Minh"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/match-tolls": {
      "post": {
        "tags": [
          "Routing"
        ],
        "operationId": "matchTolls",
        "summary": "Snap a GPS trace to the road network and list tolls passed",
        "description": "Map matching for a trip that has already been driven. Send the raw GPS points in\norder; the response is the road-accurate path plus every toll station on it,\npriced for the chosen `vehicle` class. The body uses `[longitude, latitude]`\norder — the opposite of the query-string convention.\n",
        "parameters": [
          {
            "name": "vehicle",
            "in": "query",
            "description": "Toll fee class (BOT scheme, not the Route `vehicle` profile):\n`1` cars under 12 seats, trucks under 2 t ·\n`2` buses up to 30 seats, trucks under 4 t ·\n`3` buses over 30 seats, trucks under 10 t, tractors without container ·\n`4` trucks under 18 t, tractors with ≤ 20 ft container ·\n`5` trucks 18 t and over, tractors with > 20 ft container.\n",
            "schema": {
              "type": "integer",
              "enum": [
                1,
                2,
                3,
                4,
                5
              ],
              "default": 1
            }
          },
          {
            "name": "api-version",
            "in": "query",
            "description": "API version. Optional; the current version is used when omitted.",
            "schema": {
              "type": "string",
              "enum": [
                "1.1"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "description": "GPS points in driving order as `[longitude, latitude]`. At least two.",
                "minItems": 2,
                "items": {
                  "$ref": "#/components/schemas/LngLat"
                }
              },
              "example": [
                [
                  106.757849,
                  10.817956
                ],
                [
                  106.759134,
                  10.821371
                ],
                [
                  106.760694,
                  10.825196
                ]
              ]
            }
          }
        },
        "responses": {
          "200": {
            "description": "The matched path and the tolls along it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MatchTollsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/api/maps/statics/tm": {
      "post": {
        "tags": [
          "Map display"
        ],
        "operationId": "staticMap",
        "summary": "Render a map image",
        "description": "Returns a PNG of the map centred on `lat`/`lng` with a marker and an address label.\nFor emails, reports and share previews where an interactive map is not wanted.\nParameters travel as `multipart/form-data`, including the API key.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "apikey": {
                    "type": "string",
                    "description": "Your services key. Sent in the form body for this endpoint."
                  },
                  "lat": {
                    "type": "number",
                    "format": "double",
                    "description": "Latitude of the map centre."
                  },
                  "lng": {
                    "type": "number",
                    "format": "double",
                    "description": "Longitude of the map centre."
                  },
                  "zoom": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 20,
                    "default": 15,
                    "description": "Zoom level, `0` whole world to `20` individual buildings."
                  },
                  "size": {
                    "type": "string",
                    "pattern": "^\\d+x\\d+$",
                    "default": "600x400",
                    "description": "Image size in pixels as `WIDTHxHEIGHT`."
                  },
                  "address": {
                    "type": "string",
                    "description": "Label to print on the image. Omit to reverse-geocode `lat`/`lng` on the server."
                  },
                  "markerUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "URL of a custom marker icon."
                  },
                  "markerFile": {
                    "type": "string",
                    "format": "binary",
                    "description": "Uploaded marker icon. Takes precedence over `markerUrl`."
                  }
                },
                "required": [
                  "apikey",
                  "lat",
                  "lng"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rendered map.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed parameter, for example a bad `size`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/maps/styles/{style}/style.json": {
      "get": {
        "tags": [
          "Map display"
        ],
        "operationId": "mapStyle",
        "summary": "Vector map style",
        "description": "A style document in the MapLibre / Mapbox Style Specification (version 8) that any\nGL-based SDK can load directly. Pass the full URL, including `apikey`, as the\n`style` option of VIETMAP GL JS, MapLibre GL, or the VIETMAP mobile SDKs.\n\n`tf` is different: it is an overlay, not a base map. Merge its `sources` and\n`layers` into the running map instead of passing it as `style`.\n",
        "parameters": [
          {
            "name": "style",
            "in": "path",
            "required": true,
            "description": "`tm` street (default) · `lm` light · `dm` dark · `hm` hybrid, satellite with labels ·\n`tf` real-time traffic overlay.\n",
            "schema": {
              "type": "string",
              "enum": [
                "tm",
                "lm",
                "dm",
                "hm",
                "tf"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Style JSON. Tile URLs inside already carry your `apikey`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "MapLibre / Mapbox Style Specification v8 document.",
                  "properties": {
                    "version": {
                      "type": "integer",
                      "const": 8
                    },
                    "name": {
                      "type": "string"
                    },
                    "sources": {
                      "type": "object"
                    },
                    "layers": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "sprite": {
                      "type": "string"
                    },
                    "glyphs": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "version",
                    "sources",
                    "layers"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    },
    "/maps/tiles/st/{z}/{x}/{y}.png": {
      "get": {
        "tags": [
          "Map display"
        ],
        "operationId": "satelliteTile",
        "summary": "Satellite raster tile",
        "description": "Plain satellite imagery, no labels, as a standard XYZ raster tile. Use it as a tile\nsource in Leaflet, OpenLayers, or a raster source in GL JS. For satellite imagery\nwith road labels use the `hm` vector style instead.\n\nBilling: 25 tile loads = 1 transaction.\n",
        "parameters": [
          {
            "name": "z",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "x",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "y",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tile.",
            "content": {
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "423": {
            "$ref": "#/components/responses/Locked"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apikey": {
        "type": "apiKey",
        "in": "query",
        "name": "apikey",
        "description": "Your **services key** from the VIETMAP Console. The tilemap key is rejected with `423`."
      }
    },
    "parameters": {
      "text": {
        "name": "text",
        "in": "query",
        "required": true,
        "description": "The text to search for. Send at least two characters.",
        "schema": {
          "type": "string"
        },
        "example": "197 tran phu"
      },
      "focus": {
        "name": "focus",
        "in": "query",
        "description": "The user's current position as `lat,lng`, used to rank nearby results first.",
        "schema": {
          "type": "string",
          "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"
        },
        "example": "10.762622,106.660172"
      },
      "display_type": {
        "name": "display_type",
        "in": "query",
        "description": "Address format in the response, following Vietnam's 2025 administrative merger:\n`1` new two-tier format (ward → province), `2` old three-tier format (ward → district → province),\n`3` match the format of the input, `5` new format with the old one in `data_old`,\n`6` old format with the new one in `data_new`. Use `5` when migrating from v3.\n",
        "schema": {
          "type": "integer",
          "enum": [
            1,
            2,
            3,
            5,
            6
          ]
        }
      },
      "layers": {
        "name": "layers",
        "in": "query",
        "description": "Restrict results to one data type.",
        "schema": {
          "type": "string",
          "enum": [
            "POI",
            "ADDRESS",
            "VILLAGE",
            "WARD",
            "DIST",
            "CITY",
            "STREET"
          ]
        }
      },
      "cityId": {
        "name": "cityId",
        "in": "query",
        "description": "Only return results inside this province. IDs at https://github.com/vietmap-company/vietnam_administrative_address",
        "schema": {
          "type": "integer"
        },
        "example": 12
      },
      "distId": {
        "name": "distId",
        "in": "query",
        "description": "Only return results inside this district.",
        "schema": {
          "type": "integer"
        },
        "example": 1292
      },
      "wardId": {
        "name": "wardId",
        "in": "query",
        "description": "Only return results inside this ward.",
        "schema": {
          "type": "integer"
        },
        "example": 984332
      },
      "circle_center": {
        "name": "circle_center",
        "in": "query",
        "description": "Center of a circular search area as `lat,lng`. Use together with `circle_radius`.",
        "schema": {
          "type": "string",
          "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"
        },
        "example": "10.758867,106.675566"
      },
      "circle_radius": {
        "name": "circle_radius",
        "in": "query",
        "description": "Radius of the circular search area in meters. Only applies when `circle_center` is sent.",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 200
      },
      "cats": {
        "name": "cats",
        "in": "query",
        "description": "POI category codes to filter by.",
        "schema": {
          "type": "string"
        },
        "example": "1002-1"
      },
      "admin_new": {
        "name": "admin_new",
        "in": "query",
        "description": "`true` filters by the new administrative boundaries, `false` by the old ones.\nOnly affects `cityId`, `distId` and `wardId`.\n",
        "schema": {
          "type": "boolean"
        }
      },
      "point": {
        "name": "point",
        "in": "query",
        "required": true,
        "description": "A point as `lat,lng` — latitude first. Repeat the parameter once per point, in\norder. At least two points; the maximum depends on your plan.\n",
        "style": "form",
        "explode": true,
        "schema": {
          "type": "array",
          "minItems": 2,
          "items": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$"
          }
        },
        "example": [
          "10.762622,106.660172",
          "10.801891,106.706610"
        ]
      },
      "points_encoded": {
        "name": "points_encoded",
        "in": "query",
        "description": "`true` (default) returns `points` and `snapped_waypoints` as Google polyline-5\nstrings — smaller payloads, decode client-side. `false` returns plain coordinate\narrays. Keep `true` unless you need raw coordinates.\n",
        "schema": {
          "type": "boolean",
          "default": true
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The `apikey` parameter is missing or invalid. The body is empty."
      },
      "Locked": {
        "description": "The key is out of quota, or this API is not enabled for it — for example when a\ntilemap key is used for a Map API. The body is plain text.\n",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            },
            "example": "Your request is limited"
          }
        }
      }
    },
    "schemas": {
      "Boundary": {
        "type": "object",
        "description": "One administrative unit of an address.",
        "properties": {
          "type": {
            "type": "integer",
            "description": "Level: `0` province/city, `1` district (old format only), `2` ward/commune.",
            "enum": [
              0,
              1,
              2
            ]
          },
          "id": {
            "type": "integer",
            "description": "Administrative unit ID."
          },
          "name": {
            "type": "string",
            "description": "Name without prefix, e.g. `4`."
          },
          "prefix": {
            "type": "string",
            "description": "Unit type, e.g. `Phường`, `Quận`, `Thành Phố`."
          },
          "full_name": {
            "type": "string",
            "description": "Prefix and name together, e.g. `Phường 4`."
          }
        },
        "required": [
          "type",
          "id",
          "name",
          "prefix",
          "full_name"
        ]
      },
      "SearchResult": {
        "type": "object",
        "description": "One Autocomplete or Geocode result. Has no coordinates — resolve `ref_id` with\n`/api/place/v4`.\n",
        "properties": {
          "ref_id": {
            "type": "string",
            "description": "Place reference with prefix (`auto:` or `geocode:`). Pass unchanged to `/api/place/v4`."
          },
          "distance": {
            "type": "number",
            "description": "Distance in kilometers from `focus` to the place."
          },
          "address": {
            "type": "string",
            "description": "Administrative part of the address only — ward, district (old format only),\nprovince. House number and street are in `name`.\n"
          },
          "name": {
            "type": "string",
            "description": "POI name, or house number plus street for an address, e.g. `197 Trần Phú`."
          },
          "display": {
            "type": "string",
            "description": "`name` and `address` joined into one display string."
          },
          "boundaries": {
            "type": "array",
            "description": "Administrative units from smallest to largest.",
            "items": {
              "$ref": "#/components/schemas/Boundary"
            }
          },
          "categories": {
            "type": "array",
            "description": "POI category codes. Empty for addresses.",
            "items": {
              "type": "string"
            }
          },
          "entry_points": {
            "type": "array",
            "description": "Entrances of the place, when known.",
            "items": {
              "type": "object"
            }
          },
          "data_old": {
            "description": "Same result in the old address format. Only present for `display_type=5`, otherwise `null`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/SearchResult"
              },
              {
                "type": "null"
              }
            ]
          },
          "data_new": {
            "description": "Same result in the new address format. Only present for `display_type=6`, otherwise `null`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/SearchResult"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "ref_id",
          "address",
          "name",
          "display",
          "boundaries"
        ]
      },
      "PlaceDetail": {
        "type": "object",
        "description": "Coordinates and structured address for one place. Format follows the `ref_id` prefix.",
        "properties": {
          "display": {
            "type": "string",
            "description": "Full display string."
          },
          "name": {
            "type": "string",
            "description": "POI name. Empty for a plain address."
          },
          "hs_num": {
            "type": "string",
            "description": "House or building number."
          },
          "street": {
            "type": "string",
            "description": "Street name."
          },
          "address": {
            "type": "string",
            "description": "House number and street, e.g. `197 Đường Trần Phú`."
          },
          "city_id": {
            "type": "integer"
          },
          "city": {
            "type": "string"
          },
          "district_id": {
            "type": "integer",
            "description": "`0` in the new two-tier format, which has no districts."
          },
          "district": {
            "type": "string",
            "description": "Empty in the new two-tier format."
          },
          "ward_id": {
            "type": "integer"
          },
          "ward": {
            "type": "string"
          },
          "lat": {
            "type": "number",
            "format": "double"
          },
          "lng": {
            "type": "number",
            "format": "double"
          }
        },
        "required": [
          "display",
          "lat",
          "lng"
        ]
      },
      "StatusCode": {
        "type": "string",
        "description": "Result of the request, in the body.",
        "enum": [
          "OK",
          "INVALID_REQUEST",
          "OVER_DAILY_LIMIT",
          "MAX_POINTS_EXCEED",
          "ERROR_UNKNOWN",
          "ZERO_RESULTS"
        ]
      },
      "Instruction": {
        "type": "object",
        "description": "One turn-by-turn step.",
        "properties": {
          "distance": {
            "type": "number",
            "description": "Length of this step in meters."
          },
          "heading": {
            "type": "number",
            "description": "Compass heading at the start of the step in degrees, `0` = north."
          },
          "sign": {
            "type": "integer",
            "description": "Turn code for the step."
          },
          "interval": {
            "type": "array",
            "description": "Start and end index of this step inside the decoded `points`.",
            "items": {
              "type": "integer"
            },
            "minItems": 2,
            "maxItems": 2
          },
          "text": {
            "type": "string",
            "description": "What the driver should do."
          },
          "time": {
            "type": "integer",
            "description": "Duration of this step in milliseconds."
          },
          "street_name": {
            "type": "string"
          },
          "last_heading": {
            "description": "Heading at the end of the route. Only on the final step, otherwise `null`.",
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "distance",
          "sign",
          "interval",
          "text",
          "time"
        ]
      },
      "Toll": {
        "type": "object",
        "description": "One toll station on the route. Present only when `annotations=toll` was requested.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "description": "Road or address of the station."
          },
          "type": {
            "type": "string",
            "description": "Role on the route: `entry` where the vehicle enters, `exit` where it leaves and is charged. Empty string for open-road stations."
          },
          "price": {
            "type": "integer",
            "description": "Fee at this station for the requested `vehicle`, in VND."
          },
          "prices": {
            "description": "Additional price table for this station in VND. `null` when not applicable.",
            "oneOf": [
              {
                "type": "object",
                "additionalProperties": {
                  "type": "integer"
                }
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "name",
          "price"
        ]
      },
      "Path": {
        "type": "object",
        "description": "One route.",
        "properties": {
          "distance": {
            "type": "number",
            "description": "Total length in meters."
          },
          "weight": {
            "type": "number",
            "description": "Internal routing cost the engine minimized. Unitless — use `time` and `distance` for display."
          },
          "time": {
            "type": "integer",
            "description": "Total travel time in milliseconds."
          },
          "transfers": {
            "type": "integer"
          },
          "points_encoded": {
            "type": "boolean",
            "description": "Whether `points` and `snapped_waypoints` are polyline strings or coordinate arrays."
          },
          "bbox": {
            "type": "array",
            "description": "Bounding box of the geometry as `[minLon, minLat, maxLon, maxLat]`.",
            "items": {
              "type": "number"
            },
            "minItems": 4,
            "maxItems": 4
          },
          "points": {
            "description": "Route geometry. Google polyline-5 string when `points_encoded=true`; a coordinate-pair array otherwise.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "LineString"
                  },
                  "coordinates": {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "number"
                      },
                      "minItems": 2,
                      "maxItems": 2
                    }
                  }
                }
              }
            ]
          },
          "snapped_waypoints": {
            "description": "The input points snapped to the road network, same encoding as `points`.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object"
              }
            ]
          },
          "instructions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Instruction"
            }
          },
          "toll_cost": {
            "type": "integer",
            "description": "Total toll cost in VND for the requested `vehicle`. Only with `annotations=toll`."
          },
          "tolls": {
            "type": "array",
            "description": "Toll stations along the route, in travel order. Only with `annotations=toll`.",
            "items": {
              "$ref": "#/components/schemas/Toll"
            }
          }
        },
        "required": [
          "distance",
          "time",
          "points_encoded",
          "points",
          "instructions"
        ]
      },
      "RouteResponse": {
        "type": "object",
        "properties": {
          "license": {
            "type": "string"
          },
          "code": {
            "$ref": "#/components/schemas/StatusCode"
          },
          "messages": {
            "description": "Details when `code` is not `OK`.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "paths": {
            "type": "array",
            "description": "The best route first, then alternatives if `alternative=true`.",
            "items": {
              "$ref": "#/components/schemas/Path"
            }
          }
        },
        "required": [
          "code",
          "paths"
        ]
      },
      "TspResponse": {
        "type": "object",
        "properties": {
          "license": {
            "type": "string"
          },
          "code": {
            "$ref": "#/components/schemas/StatusCode"
          },
          "messages": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "paths": {
            "type": "array",
            "description": "A single path — the optimized tour. `snapped_waypoints` are in visiting order.",
            "items": {
              "$ref": "#/components/schemas/Path"
            }
          }
        },
        "required": [
          "code",
          "paths"
        ]
      },
      "MatrixResponse": {
        "type": "object",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/StatusCode"
          },
          "messages": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "durations": {
            "type": "array",
            "description": "Travel time in seconds. `durations[i][j]` is from `sources[i]` to `destinations[j]`.",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            }
          },
          "distances": {
            "type": "array",
            "description": "Distance in meters, same layout as `durations`.",
            "items": {
              "type": "array",
              "items": {
                "type": "number"
              }
            }
          }
        },
        "required": [
          "code"
        ]
      },
      "LngLat": {
        "type": "array",
        "description": "A coordinate pair as `[longitude, latitude]`.",
        "items": {
          "type": "number"
        },
        "minItems": 2,
        "maxItems": 2
      },
      "ReverseResult": {
        "description": "One reverse-geocoding result — a SearchResult that also carries coordinates.",
        "allOf": [
          {
            "$ref": "#/components/schemas/SearchResult"
          },
          {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number",
                "format": "double"
              },
              "lng": {
                "type": "number",
                "format": "double"
              },
              "distance": {
                "type": "number",
                "description": "Distance from the queried point to this result."
              }
            },
            "required": [
              "lat",
              "lng"
            ]
          }
        ]
      },
      "MigrateResult": {
        "type": "object",
        "description": "An address rewritten into the requested administrative format.",
        "properties": {
          "address": {
            "type": "string",
            "description": "Administrative part in the target format — ward, district (old format only), province."
          },
          "name": {
            "type": "string",
            "description": "The house number and street as sent, normalised."
          },
          "display": {
            "type": "string",
            "description": "`name` and `address` joined."
          },
          "boundaries": {
            "type": "array",
            "description": "Administrative units in the target format, smallest first.",
            "items": {
              "$ref": "#/components/schemas/Boundary"
            }
          }
        },
        "required": [
          "address",
          "name",
          "display",
          "boundaries"
        ]
      },
      "MatchTollsResponse": {
        "type": "object",
        "properties": {
          "distance": {
            "type": "number",
            "description": "Length of the matched path in kilometers."
          },
          "tolls": {
            "type": "array",
            "description": "Toll stations on the path, in driving order. Empty when none.",
            "items": {
              "$ref": "#/components/schemas/Toll"
            }
          },
          "path": {
            "type": "array",
            "description": "The matched geometry as `[longitude, latitude]` pairs.",
            "items": {
              "$ref": "#/components/schemas/LngLat"
            }
          }
        },
        "required": [
          "distance",
          "tolls",
          "path"
        ]
      }
    }
  }
}