Every command the CLI ships: four auth commands, 34 API operations across 11
command groups, the tilesets-cli proxy, completion and generate-skills. Each is
shown in both of its renderings. Which one you get is decided by --output, whose default
(auto) reads stdout: a terminal gets the left column, a pipe or redirect
gets the right one. See
README's output section for the rules.
Account names, style ids and tokens in the examples are replaced; everything else is as the API sent it.
29 of the 34 were run against the live API and show what came back: 26
on 2026-09-01, and fonts list, fonts upload and fonts delete on
2026-09-08, once fonts:list/fonts:write became
registrable. The write operations were exercised as round trips on
throwaway objects — a style created, updated, drafted and deleted; icons
uploaded to a sprite and taken out again; a font uploaded and deleted —
leaving the account as it was found.
Every API command's Outputs block below is that snapshot rather than a
live reading, and is re-taken by hand — nothing schedules it and nothing
enforces it. The auth, completion, generate-skills and tilesets-cli blocks
are not captures: those are the CLI's own rendering, which the test suite does
cover.
Nothing re-checks the captures in between, because what
the Mapbox APIs return is not this repo's to monitor. What is ours — the
commands and the flags they take — is held to mapbox --schema on every
cargo test run by tests/docs_contract.rs, so the half of this page that
can be checked cannot fall behind the binary.
The remaining 5 give the response shape from the spec or the docs instead
of a live capture, for two different reasons. rasterarrays get-mrt-tile
needs a raster-array job this account does not have. The other four are
search's: read-only and safe to run, but the credentials used to write
this page have no Search Box API access, so every call answers 401 rather
than a result.
Each Parameters section lists only what is specific to its command. The globals every API command takes are in one table.
Every command, as group.command — styles.draft.get is the one that
nests, and is typed mapbox styles draft get.
Auth — auth.login · auth.logout · auth.refresh · auth.whoami
Agent skills — agent-skills.list · agent-skills.install · agent-skills.update · agent-skills.uninstall
Generate skills — generate-skills
Accounts — accounts.list-tokens · accounts.retrieve-token · accounts.list-scopes
Fonts — fonts.list · fonts.upload · fonts.delete
Geocoder — geocoder.forward · geocoder.reverse · geocoder.batch
Search — search.forward · search.reverse · search.category · search.list-category
Sprites — sprites.get-json · sprites.upload · sprites.upload-batch · sprites.delete · sprites.delete-batch
Static — static.get-image · static.get-tile
Styles — styles.list · styles.get · styles.create · styles.update · styles.delete · styles.draft.get · styles.draft.update · styles.draft.delete
Tilesets — tilesets.get-tile · tilesets.get-mvt · tilesets.get-mrt · tilesets.query
Then Errors — the shape a failure takes in each mode.
Credentials live in ~/.mapbox, one file per profile — or in whatever
directory MAPBOX_CONFIG_DIR names, when it is set.
All four commands take:
| Parameter | Effect |
|---|---|
--profile <name> |
Which credential file to act on. Default default. |
--output, -o |
auto | text | json. |
login, logout and refresh take --dry-run as well. whoami does not,
for the reason its own section gives.
Registers an OAuth client, opens the browser for the authorization code, and exchanges it for a token.
mapbox auth login # default profile
mapbox auth login --profile work # a second account, side by sideTerminal — -o text | Agent — -o json |
|---|---|
|
{"logged_in":true,"username":"user","profile":"default"} |
Progress, including the URL to visit if the browser does not open, goes to stderr in both modes.
Deletes the stored credentials file for the profile.
mapbox auth logout
mapbox auth logout --profile work -o jsonSucceeds either way; the boolean says whether there was anything to delete.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"logged_out":true,"profile":"default"} |
|
{"logged_out":false,"profile":"scratch"} |
Forces a token refresh regardless of expiry, under the per-profile credential lock.
mapbox auth refresh
mapbox auth refresh --profile work --debugTerminal — -o text | Agent — -o json |
|---|---|
|
{"expires_at":1788276540,"profile":"default","refreshed":true} |
Reports the token the next command will use and whose it is, which is not always the login you remember making.
It reads the stored credentials without refreshing them, so asking who you are cannot spend the single-use refresh token or wait on another invocation's lock. A stored token already inside the five-minute refresh window is therefore reported as expiring — the next real command is what refreshes it.
Everything reported below is read out of the token locally, and reading a token cannot tell a revoked one from a live one: a revoked token still carries a readable account and a future expiry.
| Parameter | Effect |
|---|---|
--verify |
Ask Mapbox about the token instead of only reading it. |
--verify adds a Verified: TokenValid line, and turns every other verdict
into a failure with a code of its own — token_expired, token_revoked,
token_invalid, token_malformed — so a script can tell a token that needs
re-issuing from one that was never a token at all. It is the only part of
this command that makes a request.
There is no --dry-run: the command reads the store and reports, and
--verify asks about a token rather than changing one.
mapbox auth whoami
mapbox auth whoami --profile work
mapbox auth whoami --verifyTerminal — -o text | Agent — -o json |
|---|---|
|
{"account":"user","env_var":null,"expires_at":1788276540,"profile":"default","source":"login","stored_login":"user","usage":"tk","verified":null} |
MAPBOX_ACCESS_TOKEN outranks a login, and the report names the source it
resolved rather than whichever it happened to read first. A Login: line
appears when there is a stored login that is not the one in use, and the
mismatch is warned about on stderr in both modes:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"account":"someone-else","env_var":"MAPBOX_ACCESS_TOKEN","expires_at":null,"profile":"default","source":"environment","stored_login":"user","usage":"pk","verified":null} |
With nothing to report it exits non-zero under the code
not_authenticated, so mapbox auth whoami && … means what it looks like:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"not_authenticated","docs":["https://docs.mapbox.com/api/accounts/tokens/"],"fix":"Run `mapbox auth login`, export MAPBOX_ACCESS_TOKEN, or pass `--token`.","message":"No Mapbox token available.","next_actions":["mapbox auth login"]} |
34 operations across 11 command groups. Ten are generated from the OpenAPI specs
vendored in openapi/; search is the one exception — a hand-authored
spec versioned in this repo's own custom-openapi/, see
custom-openapi/README.md.
A command group is not a spec file. Which command group an operation belongs to is
decided per operation, by an x-mapbox-cli-command extension the sync
writes onto it, not by which file it was parsed from — so sprites is
five operations out of the styles spec, and tilesets is one operation
each out of the raster-tiles and vector-tiles specs. Two names that used to
be command groups, maps and vectortiles, are gone because their last
operation moved to tilesets.
| Command group | Operations |
|---|---|
accounts |
3 |
fonts |
3 |
geocoder |
3 |
rasterarrays |
1 |
search |
4 |
sprites |
5 |
static-images |
3 |
static-tiles |
1 |
styles |
8 |
tilequery |
1 |
tilesets |
2 |
The Contents list above names every one of the 34.
Everything else the Mapbox specs describe is not here at all. Not
hidden, not shipped as a command that refuses: absent from the spec content
this binary compiles against.
A maintainer-only decision record tracks which operations are enabled, and
the vendoring step that derives openapi/ strips everything it does not
mark enabled. So an operation left out answers exactly as a mistyped
name does, and there is nothing in --help, --schema or this page to
suggest otherwise.
Three kinds of reason sit behind those decisions, and they are worth telling apart:
- No token can carry the scope.
POST /oauth/registerrefusesfonts:metadata,tokens:writeandstyles:download, so a login can never obtain them.src/spec.rs'sUNSUPPORTED_OPERATIONSrecords which operations need one, and an entry comes off that list once the scope becomes registrable — nothing here can force that.fonts:listandfonts:writeused to be on that list — both became registrable on 2026-09-08, which is what shippedfonts list,fonts uploadandfonts delete.styles download-style-zip's real blocker turned out to be one level deeper: production answers it 403 "This is a prerelease API. Please contact support," regardless of scope — access is granted per-account by Mapbox support, not by OAuth, so addingstyles:downloadto the allowlist would not unblock it by itself. - Withheld deliberately.
styles set-style-protectedunlocks a style for deletion — a live token holdingstyles:protect(which is registrable) can call it successfully, this CLI just declines to offer a one-line command with no confirmation for something that dangerous; a couple ofstylesoperations are admin-only and gated by role rather than by scope — a normal login gets a bare 403 with no scope named, even for an account that belongs to Mapbox;fonts get-model-assetonly needsfonts:read, already granted, but reaches a product surface nothing here can exercise — no reachable account has a model to fetch; the raster-tiles spec'sget-legacy-gridandget-legacy-tileare the retired v1–v3 API, which cannot succeed with any token this CLI can hold.WITHHELD_OPERATIONSinsrc/spec.rskeeps the reasons next to the names. - Curated out of the spec itself.
search'ssuggestandretrieve/{id}are in neither list — they were never written intocustom-openapi/search/openapi/search.yamlto begin with, since both need a caller-managedsession_tokenfor a client-side autocomplete flow this CLI has no one-shot equivalent of. A reader followingUNSUPPORTED_OPERATIONSorWITHHELD_OPERATIONSintosrc/spec.rsto ask why finds nothing about either name; the answer is in the spec file itself.
Both src/spec.rs lists still filter what reaches the command tree, and
both currently remove nothing: the sync that derives openapi/ already
took every operation they name out of it. They stay because a later sync
can re-enable one, and because the reason an endpoint is unusable belongs
beside the endpoint.
Several command groups also define a liveness probe, at the group's root or a
conventional health-check path. Those are for whatever monitors it
rather than for a caller, so none is a command — and none is in openapi/
either.
| Parameter | Effect |
|---|---|
--token, -t |
Access token. Falls back to MAPBOX_ACCESS_TOKEN, then stored credentials. |
--username, -u |
Fills {username}/{owner}/{account} path placeholders. Falls back to MAPBOX_USERNAME, then to the logged-in user — so it can be left off entirely once you are signed in. Examples below pass it anyway, to show where it lands. |
--use-login |
Ignore MAPBOX_ACCESS_TOKEN; use stored credentials. |
--profile <name> |
Which stored credentials to use. |
--output, -o |
auto | text | json. |
--id <value> |
On a command that returns a list, print just the row with that id or name. |
--timeout <seconds> |
How long one request may take, connection included. Defaults to 60 seconds, or 900 for a body read from --file or from a --data @<path>/@-. Also MAPBOX_TIMEOUT. |
An operation with a request body takes --data/-d when that body is text
the caller types — JSON for most, a bare true/false for star-file —
and --file <PATH> when it is bytes: raw for application/octet-stream and
image/svg+xml, one repeatable part for multipart/form-data. Two
operations declare both and reject having both passed. Each command's own
Parameters below lists only what is specific to it.
--data does not have to carry the body itself. Following curl,
@<path> reads a file and @- reads stdin:
mapbox styles create --data @style.json
jq '.name = "Renamed"' style.json | mapbox styles update STYLE_ID --data @-Only the first character decides, so --data '{"contact":"a@b.example"}' is
still the body it looks like. A body whose first character is a literal @
cannot be passed this way — curl has the same limitation, and it costs
nothing here because every operation that takes --data sends JSON, and @
is not valid JSON.
Three things worth knowing about the read forms:
- The body is sent byte for byte. The trailing newline a text editor leaves is insignificant to a JSON parser and is not stripped, because trimming a body the caller supplied would be the CLI editing what it was asked to send.
- The timeout changes with it. A typed
--datais capped by the command line at a megabyte or so and gets the 60-second budget;@<path>and@-are unbounded and get the same 900 seconds--filedoes. @-suppresses the confirmation on a delete. Of the five operations that take--data, onlymapbox sprites delete-batchis aDELETE, and a question needs stdin to be a terminal — which a pipe is not. So piping a body into it sends it unasked, exactly as< filealways did. Use@<path>rather than@-to keep the prompt, or pass--yesto say the answer deliberately.
A command that changes something takes --dry-run, which prints the request
it would send, on stdout, and sends nothing. Which commands those are is not
a list anyone keeps: it is every POST, PUT, PATCH and DELETE — 12 of
the 34 operations — plus auth login, auth logout, auth refresh and
generate-skills. A read-only GET does not take it, so mapbox styles list --dry-run is a usage error rather than a no-op. It rehearses
rather than describes: --data is parsed and every --file is read, so a
body that will not parse or a path that cannot be read fails under it too.
Like --data, it goes after the operation name.
Two shapes, one with almost nothing and one with most of it:
mapbox geocoder forward --q Helsinki
mapbox --use-login --profile work styles create --username user \
--data '{"name":"My Style","version":8,"sources":{},"layers":[]}'Under json a response goes out as one compact line, untouched — indented
instead when -o json is asked for at a terminal, since then a person is
reading it.
-o json promises that everything on stdout is JSON. It does not promise how
many documents: a command with one result emits one, and a command that
streams emits one per line — JSON Lines. Which one you get is a property of
the command, not of the flag, the same way a command that answers with a PNG
answers with a PNG in every mode. There is no -o jsonl. No command streams
today; when one exists it will say so in its own section here, and every
command that does not stream will keep emitting exactly one document, which
the test suite checks on every run.
Under text it is rendered by shape, decided from the response itself
rather than from the spec:
| Response | Rendered as |
|---|---|
| An array of like objects | A table, one row each |
| One key holding an array of like objects | The same table — {"icons":[…]} is still a listing |
| A single object | A field list, one level of nesting flattened onto dotted keys |
| No body at all | A confirmation naming what happened — Deleted <id>., or {"ok":true,…} |
A search, geocoder or tilequery FeatureCollection |
A numbered list, one entry per feature — see the paragraph below |
| Anything else | Pretty-printed JSON — every other command group's GeoJSON, style documents and bare values lose their meaning in a table |
A table shows the columns most rows have, that vary, and that do not repeat another column; identifiers keep their full width and everything else narrows to fit. Underneath it says what it clipped and how to see one row whole.
search's GeoJSON FeatureCollection is a deliberate exception to
"anything else": forward, reverse and category each return a list of
POIs meant to be scanned, so it renders — rather than staying JSON the way
most other command groups' GeoJSON does. Not as a table, though: a table's
column is one fixed width for every row, and a street address is exactly
the field that width can't be chosen for without clipping almost every one
of them to a few characters and an ellipsis. So it's a numbered list
instead — name, its POI category (or feature_type for a result with
none, e.g. an address) and distance on one line, the whole address on the
next, longitude,latitude on the one after that (the next thing a caller
usually wants a result for), never clipped:
1. Golden Gate Bridge (bridge, landmark) — 8710.3 km
Golden Gate Bridge, Sausalito, California, United States
-122.4783,37.8199
2. Kalve Coffee Golden Gate (café, coffee, coffee shop) — 91.3 km
Ahtri tn 6, 10151 Tallinn, Estonia
24.7454,59.437
geocoder's and tilequery's FeatureCollections render as a numbered list
for the same reason — see their own sections for the shape. The match is on
those three command-group names exactly, so another command group that answers with
GeoJSON keeps falling to pretty-printed JSON; its nesting is the information
a list or table would throw away. A feature whose properties give it nothing
to show falls the whole collection back to JSON rather than print a blank
numbered entry — a feature that also carries a geometry still keeps its
coordinate line, since the guard checks what the row ended up with, not the
properties directly. A conforming response never reaches that case:
geocoder requires name/feature_type on every feature, tilequery
requires tilequery.layer.
Four of the eleven command groups can answer with bytes — rasterarrays,
static-images, static-tiles and tilesets. Those bypass --output in
both modes:
| Terminal — refuses | Redirected — raw bytes |
|---|---|
|
|
Several listings are paginated by the API, which returns one page and a
Link header naming the next. The CLI says so rather than leaving the
result looking complete, and names the flags that fetch the next page:
$ mapbox accounts list-tokens --username user --limit 2
ID NOTE CREATED USAGE
cmtoken00000000000000001a CI deploy key 2026-09-04 sk
cmtoken00000000000000002b Local dev 2026-09-03 pk
Tips:
`-o json` for the response as the API sent it.
To see one row: add `--id cmtoken00000000000000001a`
More results: add `--limit 2 --start cmtoken00000000000000002b` for the next page.
Under -o json the same note is the only thing printed to stderr on a
success, and as the lone tip it takes the singular form:
$ mapbox accounts list-tokens --username user --limit 2 -o json > page1.json
Tip: More results: add `--limit 2 --start cmtoken00000000000000002b` for the next page.
The flags are derived from the response, not hardcoded: whatever the spec calls an operation's paging parameters is what the line names. The access token is never among them, even though the API echoes it back in that header.
Two details worth knowing:
-
The note goes to stderr in both modes, including
-o json. The result is just as partial there, and the API's own document cannot carry the fact without an envelope this CLI has promised not to add — so a-o jsonconsumer reading stdout alone is unaffected, and one watching stderr is told. There is no--allyet; following the pages is the caller's job. -
--idsearches the page it was given. On a paginated listing a miss means "not on this page", which is not the same as "does not exist", so the error says which and how to look further:Error: No row has the id `cmtoken00000000000000009z`. Fix: This is one page of results, so the id may be on a later one. Add `--start cmtoken00000000000000002b --limit 2` to search the next page.
Mapbox Tokens API. Three operations; the three that write tokens need
tokens:write, which is not registrable.
Lists the access tokens for an account. Secret (sk) entries omit the
token string. Needs tokens:read.
| Parameter | Effect |
|---|---|
--limit <n> |
How many to return. |
--start <id> |
Continue after this token id — the paging cursor. |
--sortby <created|modified> |
Sort order. |
--usage <pk|sk|tk> |
Only tokens of that kind. |
--default |
Only the account's default token. |
Results are paginated. When more exist the CLI prints the --start value to
continue from — see One page at a time.
mapbox accounts list-tokens --username user
mapbox accounts list-tokens --username user --limit 2
mapbox accounts list-tokens --username user --usage sk --sortby created
mapbox accounts list-tokens --username user --id cmtoken00000000000000001aTerminal — -o text | Agent — -o json |
|---|---|
|
[{"client":"api","created":"2026-06-10T09:10:53.850Z","default":false,"id":"cmtoken00000000000000001a","note":"CI token","scopes":["styles:read","fonts:read"],"usage":"pk"}] |
The API omits the token string from sk rows, so a secret token's string
never comes back at all. pk and tk rows do carry it, and a listing where
most rows have one — --usage pk, say — gets a TOKEN column, clipped to
the column width like any other value.
--usage and --limit together return an empty list from the API,
whatever the limit is:
mapbox accounts list-tokens --username user --usage pk # 3 rows
mapbox accounts list-tokens --username user --usage pk --limit 10 # 0 rowsSame from curl, so it is the Tokens API rather than the CLI. Use one or
the other.
Checks whether a token is valid and reports what it carries. The token being checked is the one the command authenticates with, so this is "what am I holding" rather than a lookup by id.
mapbox accounts retrieve-token
mapbox accounts retrieve-token --token pk.eyJ1Ijoi…
mapbox accounts retrieve-token --profile work -o jsonAlways HTTP 200 — code carries the verdict: TokenValid,
TokenMalformed, TokenInvalid, TokenExpired or TokenRevoked.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"TokenValid","token":{"client":"9f3c1ab2…7d40e6c8","created":"2026-09-01T15:50:16.000Z","expires":"2026-09-01T23:50:16.000Z","scopes":["styles:tiles","styles:read","styles:write"],"usage":"tk","user":"user"}} |
The scope list is the thing worth reading here: a 403 from any other command is usually a scope missing from this list.
Lists the token scopes the account may request. Needs scopes:list.
mapbox accounts list-scopes --username user
mapbox accounts list-scopes --username user --id styles:downloadTerminal — -o text | Agent — -o json |
|---|---|
|
[{"description":"List all available scopes.","id":"scopes:list"},{"description":"Read styles.","id":"styles:read"}] |
What this lists is what the account is allowed to hold, which is not the
same as what the current token holds — retrieve-token answers that.
The fonts an account owns. Three operations.
All three need fonts:list or fonts:write, which became registrable
on 2026-09-08. All three were run live on 2026-09-08 with a token
from mapbox auth login — a round trip: uploaded, listed, deleted — and
this page's captures below are that run. Anyone who ran mapbox auth login
before this release needs to run it again: the scope set is fixed when the
login client registers, so a refreshed token cannot pick up a scope added
afterward.
The font faces an account owns.
--fresh skips the cache. Worth knowing even outside a script: a font
just uploaded does not appear without it — the API's own Cache-Control
otherwise serves a listing from before the upload for some window
afterward.
mapbox fonts list --username user
mapbox fonts list --username user -o jsonTerminal — -o text | Agent — -o json |
|---|---|
|
["Open Sans Regular"] |
An account with nothing uploaded:
(none)
Tip: `-o json` for the response as the API sent it.
Upload a font face. Requires fonts:write.
--file is the font's own bytes (.ttf, .otf, or similar), sent raw —
not JSON, not multipart. Roughly 30MB is the API's own limit.
--dry-run reads the file to confirm it exists and is readable, and prints
the request it would send, without uploading anything.
mapbox fonts upload --file "./Open Sans Regular.ttf" --username userTerminal — -o text | Agent — -o json |
|---|---|
|
{"family_name":"Open Sans","hash":"0000000000000000000000000000000000000a","owner":"user","style_name":"Regular","visibility":"private"} |
Uploading again — same account, same face — succeeds the same way rather than conflicting; the second upload replaces the first.
Delete a font face. Requires fonts:write. Asks for confirmation at a
terminal, like every DELETE; --yes skips it.
<face> is the full face name, family and style together — the same
spelling fonts list reports.
--dry-run prints the request without sending it and without asking.
mapbox fonts delete "Open Sans Regular" --username userTerminal — -o text | Agent — -o json |
|---|---|
|
{"command":"fonts delete","ok":true,"status":204} |
Returns 204 whether or not the font existed, so a second delete of the same face answers exactly the same way — there is nothing in the response body to tell the two apart.
Places to coordinates and back. Geocoding v6.
All three return GeoJSON, rendered as a numbered list under -o text —
name and feature type on one line, the full address on the next,
longitude,latitude on the one after that (the next thing a caller
usually wants a result for), never clipped. The response's attribution,
the terms the results come under, follows the list — once under batch's
whole result rather than once under each of up to fifty identical copies.
batch gets one such list per query, under a Query N: header — omitted
when the batch held a single query, since there is nothing to tell it apart
from.
Looks up a location from search text, and returns its standardized address, geographic context and coordinates.
| Parameter | Effect |
|---|---|
--q <text> |
The search string. |
--limit <n> |
How many results. Default 5, max 10. |
--country <codes> |
ISO 3166-1 alpha-2, comma-separated. |
--types <types> |
address, place, postcode, poi, … |
--proximity <lon,lat> |
Bias results towards a point. |
--bbox <minlon,minlat,maxlon,maxlat> |
Restrict to a box. |
--language <tag> |
IETF language tag. |
--autocomplete |
Partial-input matching. |
--worldview <code> |
Which country's view of disputed borders. |
--permanent |
The result may be stored. Billed differently. |
--format <format> |
geojson (default), or v5 for the older response shape. |
--entrances |
Include the building entrances of address features. Public Preview. |
Structured input is an alternative to --q: --address-number,
--street, --place, --region, --postcode, --locality,
--neighborhood, --address-line1, --block. Do not combine them with
--q.
mapbox geocoder forward --q Helsinki --limit 1
mapbox geocoder forward --q "1600 Pennsylvania Ave" --country us --types address
mapbox geocoder forward --street "Kaivokatu" --place Helsinki --country fiTerminal — -o text | Agent — -o json |
|---|---|
|
{"type":"FeatureCollection","attribution":"NOTICE: © 2026 Mapbox and its suppliers. All rights reserved. This response and the information it contains may not be retained.","features":[{"geometry":{"coordinates":[24.941822,60.167507],"type":"Point"},"properties":{"name":"Helsinki","feature_type":"place","full_address":"Helsinki, Uusimaa, Finland"}}]} |
Looks up the features at a pair of coordinates.
--longitude and --latitude are required. --limit, --types,
--country, --language, --worldview and --permanent narrow the result
the same way they do for forward geocoding.
mapbox geocoder reverse --longitude 24.94 --latitude 60.16
mapbox geocoder reverse --longitude -74.0 --latitude 40.7 --types addressA negative coordinate is a value, not a flag. That took a fix — clap read
-74.0 as a cluster of short options and rejected the command, which made
every coordinate west of Greenwich unusable.
The same numbered list as forward geocoding.
Up to 50 forward or reverse queries in one request. Each query is an object in a JSON array, with what would have been query parameters as its fields.
--data/-d carries the array. --permanent applies to the whole batch.
mapbox geocoder batch -d '[
{"types":["place"],"q":"Helsinki"},
{"types":["place"],"q":"Tampere"},
{"longitude":24.94,"latitude":60.16}
]'A batch array, one entry per query, in the order sent. Under -o text
each query's results render as their own numbered list, same as
forward/reverse, under a Query N: header — dropped
when there is only one query, where the header names the only thing on
screen:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"batch":[{"type":"FeatureCollection","features":[…],"attribution":"NOTICE: …"},{"type":"FeatureCollection","features":[…],"attribution":"NOTICE: …"}]} |
Every entry carries its own attribution and it is always the same notice,
so it is printed once under the whole batch. Two that genuinely differed
would both be shown.
A query malformed enough that its own list can't be built falls the whole batch back to pretty-printed JSON, same as one broken feature does for a single query.
The public, non-interactive surface of the Search Box API: text search,
reverse lookup, category search, and the category list. Curated by hand
down to the parameters documented at docs.mapbox.com/api/search/search-box
— see custom-openapi/README.md for why this command group doesn't come from
the vendored specs the way the others do. suggest and
retrieve/{id} are deliberately not here: both need a caller-managed
session_token to group a client-side autocomplete flow — a UX built
around a person typing into a search box, not a one-shot CLI invocation —
so there is no non-interactive way to use them.
vs. geocoder: geocoder reverse and search reverse take
nearly identical coordinates and answer different questions. geocoder
returns canonical addresses and administrative hierarchy (country, region,
postcode, place); search returns POIs and businesses with the metadata a
geocoder has no field for — rating, price level, hours of operation, brand.
Reaching for the wrong one for a query the other answers is the most likely
mistake here: "what's the address at this point" is geocoder, "what's
near this point" is search.
Text search for an address or POI — the one-off equivalent of typing into a search box and taking the first screen of results, with no autocomplete session behind it.
| Parameter | Effect |
|---|---|
--q <text> |
The search string. Required. |
--limit <n> |
How many results, up to 10. |
--proximity <lon,lat> | ip |
Bias results towards a point, or the caller's IP location. |
--near <text> |
Bias results towards a place described in free text, e.g. "paris france". |
--bbox <minlon,minlat,maxlon,maxlat> |
Restrict to a box. |
--radius <degrees> |
Restrict to a radius around --proximity. |
--country <codes> |
ISO 3166-1 alpha-2, comma-separated. |
--types <types> |
poi, address, place, … |
--poi-category <cats> / --poi-category-exclusions <cats> |
Include or exclude POI categories. |
--show-closed-pois / --open-now |
Include closed POIs, or only currently-open ones. |
--minimum-rating <0.0-5.0> / --price-levels <$..$$$$> |
Filter POIs by rating or price. |
--exclude-fields <fields> |
Omit metadata fields from the response, e.g. photos,reviews. |
--rank-strategy <distance|relevance> |
Change how results are ordered. |
--language <tag> |
ISO language code. |
--auto-complete |
Include partial and fuzzy matches, for autocomplete-style input. |
--sar-type isochrone + --route <polyline> + --route-geometry <polyline|polyline6> |
Search-along-route: results near a route rather than a point. |
--time-deviation <minutes> |
With SAR, maximum detour allowed from the route. |
--eta-type navigation + --navigation-profile <driving|walking|cycling> + --origin <lon,lat> |
Include an ETA in each result, from --origin (or --proximity) to it. |
mapbox search forward --q "34170 Gannon Terrace" --limit 1
mapbox search forward --q coffee --proximity -121.90662,37.42827 --poi-category coffeeNot captured live: the credentials used to write this doc have no Search
Box API access (401). properties below is
the documented example;
the text column is the actual list this response renders as (see "How a
response is rendered" — search is one of the three command groups whose GeoJSON
becomes a list rather than staying pretty-printed):
Terminal — -o text | Agent — -o json |
|---|---|
|
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"coordinates":[-122.059627,37.56153],"type":"Point"},"properties":{"name":"34170 Gannon Terrace","mapbox_id":"{mapbox_id}","feature_type":"address","full_address":"34170 Gannon Terrace, Fremont, California 94555, United States","distance":20045}}]} |
The POIs and addresses at a coordinate — search's counterpart to
geocoder reverse, answering with business metadata instead of
administrative hierarchy.
--longitude and --latitude are required.
| Parameter | Effect |
|---|---|
--limit <n> |
How many results, up to 10. |
--country <codes> |
ISO 3166-1 alpha-2, comma-separated. |
--types <types> |
poi, address, place, … |
--show-closed-pois |
Include permanently closed POIs. |
--language <tag> |
ISO language code. |
mapbox search reverse --longitude -118.471383 --latitude 34.023653 --limit 1Not captured live, for the same reason as forward above. Listed the same
way; properties is per
the docs.
No distance here — the docs' own example doesn't return one for reverse:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"coordinates":[-118.471584,34.023345],"type":"Point"},"properties":{"name":"1827 21st Street","feature_type":"address","full_address":"1827 21st Street, Santa Monica, California 90404, United States"}}]} |
POIs in a canonical category, near a location or along a route — quick "buttons" like a coffee search, rather than a text query.
<category> (positional) is the canonical category ID, e.g. coffee —
see list-category below for the full list. One of --proximity,
--near, --bbox or --route is required by the API, though nothing
here enforces it before the request goes out.
| Parameter | Effect |
|---|---|
--proximity <lon,lat> | ip |
Search near a point, or the caller's IP location. |
--near <text> |
Search near a place described in free text. |
--bbox <minlon,minlat,maxlon,maxlat> |
Restrict to a box. |
--radius <degrees> |
Restrict to a radius around --proximity. |
--limit <n> |
How many results, up to 25. |
--country <codes> |
ISO 3166-1 alpha-2, comma-separated. |
--types <types> |
poi, address, place, … |
--poi-category-exclusions <cats> |
Exclude POI categories. |
--show-closed-pois |
Include permanently closed POIs. |
--exclude-fields <fields> |
Omit metadata fields from the response, e.g. photos,reviews. |
--language <tag> |
ISO language code. |
--sar-type isochrone + --route <polyline> + --route-geometry <polyline|polyline6> |
Search-along-route: results near a route rather than a point. |
--time-deviation <minutes> |
With SAR, maximum detour allowed from the route. |
--eta-type navigation + --navigation-profile <driving|walking|cycling> + --origin <lon,lat> |
Include an ETA in each result, from --origin (or --proximity) to it. |
mapbox search category coffee --proximity -121.90662,37.42827 --limit 1
# --route takes an encoded polyline — the same format the Directions API's
# `routes[].geometry` returns (geometries=polyline by default; pass
# --route-geometry polyline6 if you fetched one with geometries=polyline6):
mapbox search category gas_station \
--route '_fmcFn{`gV`AdGCT~AbJBf@C`@i@dBK~@iBn@{@c@' \
--sar-type isochrone --limit 2Not captured live, for the same reason as forward above. Listed the same
way; properties is per
the docs.
poi_category is what shows in place of feature_type, joined —
category search results almost always have one; brand is not shown at
all (nothing here is a good enough summary of it to put on one line), and
-o json is where it is:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"coordinates":[-122.6180785,38.9307594],"type":"Point"},"properties":{"name":"Starbucks","feature_type":"poi","brand":["Starbucks"],"poi_category":["café","coffee","coffee shop"],"full_address":"15885 Dam Road, Clearlake, California 95422, United States","distance":19568}}]} |
The canonical category IDs usable with search category, with a display
name in the requested language. Does not describe parent/child
relationships between categories.
None beyond --language.
mapbox search list-category
mapbox search list-category --language frNot captured live, for the same reason as forward above. Renders as a
table, not the list forward/reverse/category get — every category is
three short strings, none of them long enough to need a line of its own —
built from canonical_id, name and icon; uuid and version are left
out, both generated fresh each request and no use as identifiers. Shape
per the docs:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"listItems":[{"canonical_id":"food_and_drink","icon":"fast-food","name":"Food and Drink","uuid":"71fed985-…","version":"25:6bd9…"},{"canonical_id":"lodging","icon":"lodging","name":"Lodging","uuid":"8de7b125-…","version":"25:6bd9…"}],"attribution":"…"} |
The sprite sheet a style draws its icons from, and the individual icons in it. Five operations.
These are Styles API endpoints — every URL here sits under
styles/v1/{username}/{style_id}/sprite, and a <style-id> positional is
the first argument of all five. They are a command group of their own
rather than five more styles commands because a sprite is a different
thing from a style, and because mapbox styles --help read as two
command groups stacked on top of each other while it held both.
The sprite index: where each icon sits in the sheet, and how big it is.
mapbox sprites get-json ckstyle00000000000000001a --username user
mapbox sprites get-json ckstyle00000000000000001a --username user -o json > sprite.jsonmax-age=900, so for up to fifteen minutes after an upload
or delete it keeps answering with the sprite as it was. The mutation
commands all return the index themselves, from the origin; use that.
The response is one object keyed by icon name, not an array, so it becomes a
field list — one line per icon property, with the icon name as the prefix.
For a style with a few hundred icons that is several thousand lines, and the
key column is padded to the longest icon name. -o json is the usable form.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"ae-d-route-3":{"height":24,"pixelRatio":1,"placeholder":[0,9,24,15],"visible":true,"width":24,"x":78,"y":124}} |
Adds one SVG to a style's sprite, under the icon name given.
--file <PATH> is the SVG. The body is a raw image/svg+xml, so the file's
bytes go out untouched — --data does not apply here.
Limits: 512 px per side, under 400 KB, 1,000 images per sprite, icon names up to 255 characters.
mapbox sprites upload ckstyle00000000000000001a zz-clitest-1 \
--username user --file icon.svgEvery one of the four sprite commands answers with the whole sprite
index, not with the icon you touched — 440 entries for the style above,
one line of 45 KB under json and some 2,900 lines under text. Rows for
the new icon, from a real upload:
Terminal — -o text | Agent — -o json |
|---|---|
|
{…,"zz-clitest-1":{"height":16,"pixelRatio":1,"visible":true,"width":16,"x":480,"y":409}} |
x and y are where the icon landed in the sheet, which is the point of
getting the whole index back: the sprite has been repacked, so every other
icon's coordinates may have moved too.
The index a mutation returns is the only fresh view of the sprite.
sprites get-json is a GET behind CloudFront with max-age=900, so for up to
fifteen minutes after an upload it keeps answering with the sprite as it was
— same entry count, none of the new icons. Verified: a sprites upload-batch
answered with 444 entries including all four new icons while
sprites get-json still said 440 with none, and the response carried
x-cache: Hit from cloudfront. Read the mutation's own response; do not
confirm a write by reading it back.
The -o json hint under the field list goes to stderr, so
... -o text > sprite.txt still gets a clean file.
Re-uploading an existing name overwrites it and answers 200 — there is no "already exists" error, and nothing warns you.
A file that is not an SVG is refused by the API, not locally:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_422","message":"Image is invalid. Must be a valid SVG.","status":422} |
A path that does not exist is caught before any request goes out:
{"code":"invalid_file","message":"Cannot read `/tmp/nope.svg` given to --file: No such file or directory (os error 2)"}Adds up to 25 SVGs in one request, 100 KB each.
--file <PATH>, repeated once per image. Each becomes a part in a
multipart/form-data body under the field the spec names, images.
The icon name comes from the filename, not from an argument:
zz-clitest-3.svg becomes the icon zz-clitest-3. There is no way to
upload a batch under names that differ from the files.
mapbox sprites upload-batch ckstyle00000000000000001a --username user \
--file zz-clitest-3.svg --file zz-clitest-4.svgThe whole sprite index again, with the uploaded icons in it. From a real run of the two files above:
Terminal — -o text | Agent — -o json |
|---|---|
|
{…,"zz-clitest-3":{"height":16,"pixelRatio":1,"visible":true,"width":16,"x":0,"y":425},"zz-clitest-4":{…,"x":16,"y":425}} |
A batch lands on a fresh row — y: 425, where the single upload above went
to y: 409 — because the sheet was repacked to fit them.
Removes one icon from the sprite.
mapbox sprites delete ckstyle00000000000000001a zz-clitest-1 --username userThe sprite index after the delete — the named icon is gone from it, and everything else is still there. A 200 with the full layout, not a 204: the absence of the icon is the only acknowledgement there is.
Terminal — -o text | Agent — -o json |
|---|---|
|
{…,"zz-clitest-2":{…},"zz-clitest-3":{…},"zz-clitest-4":{…}} |
zz-clitest-1 is absent; -2 onwards remain. Note that -2 has moved to
x: 480, the slot -1 occupied — a delete repacks the sheet too.
An icon name that is not in the sprite is a 404:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_404","message":"Sprite not found","status":404} |
Removes up to 150 icons in one request.
--data/-d is a JSON array of icon names — not an object.
mapbox sprites delete-batch ckstyle00000000000000001a --username user \
-d '["zz-clitest-5","zz-clitest-6"]'The sprite index after the deletions, as with the single delete. Captured by uploading two icons and removing them again: the response went from 442 entries with both present to 440 with neither.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"ae-d-route-3":{…},…} // 44,859 bytes, 440 entries, no zz-clitest-* |
Every name must exist. One that does not takes the whole request down with a 404 that names it — the deletions are not partial-applied:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_404","message":"Image \"zz-clitest-nonexistent\" not found","status":404} |
An empty array is refused, so there is no no-op form:
{"code":"http_422","message":"Remove at least 1 image","status":422}And an object where an array belongs:
{"code":"http_400","message":"Body must be an array of image names.","status":400}A rendered map image or raster tile from a style. Both return image bytes,
so --output does not apply — redirect to a file. Static Images and Static
Tiles merged into this one command group (#116); get-image and get-tile
are what were static-images get-static-image and static-tiles get-static-tile.
<format> takes a leading dot — .png, .jpeg, .webp, or "" for
the style default. <highRes> is @2x or "". <overlay> (get-image
only) is a marker/path/GeoJSON expression, or "" for none.
"" for the overlay drops the segment entirely rather than sending an empty
one, so a plain map image needs no overlay expression.
A map image centred on a point.
Eleven positionals, in order: <style-id> <overlay> <lon> <lat> <zoom> <bearing> <pitch> <width> <height> <highRes> <format>.
<bearing> and <pitch> default to 0 at the API, but the CLI requires
both as integers — pass 0 0 rather than "" "".
| Parameter | Effect |
|---|---|
--attribution / --logo |
Keep or drop the Mapbox attribution and logo. |
--addlayer <json> |
Add one layer on top of the style. |
--before-layer <id> |
Where to insert it. |
--setfilter <json> / --layer-id <id> |
Filter an existing layer. |
mapbox static get-image streets-v12 "" 24.94 60.16 12 0 0 600 400 "" .png \
--username mapbox > map.png
mapbox static get-image streets-v12 "pin-s+555555(24.94,60.16)" \
24.94 60.16 12 0 0 600 400 "@2x" .png --username mapbox > pin.png| Terminal — refuses | Redirected — raw bytes |
|---|---|
|
|
One raster tile rendered from a style, rather than from a tileset.
Seven positionals: <style-id> <tilesize> <z> <x> <y> <highRes> <format>.
<tilesize> is 512 (the default) or 256. 512 px tiles at zoom z cover
the same ground as 256 px tiles at z+1, so 256 needs four times as many
requests for the same area.
<highRes> is @2x or ""; <format> takes a leading dot, as in
get-image.
mapbox static get-tile streets-v12 512 2 1 1 "" .png \
--username mapbox > tile.png
mapbox static get-tile streets-v12 256 12 2048 1361 "@2x" .png \
--username mapbox > tile@2x.png| Terminal — refuses | Redirected — raw bytes |
|---|---|
|
|
Styles and their drafts. Eight operations.
The three draft commands sit under a draft group of their own —
mapbox styles draft get, not mapbox styles get-style-draft — because
the draft is a second version of the same style rather than a second kind
of thing, and the three read as a set. mapbox styles draft --help lists
them.
The sprite endpoints share these URLs and are a command group of their own.
Metadata for every style in an account.
| Parameter | Effect |
|---|---|
--limit <n> |
How many to return. |
--start <style-id> |
Continue after this style — the paging cursor. |
--draft |
Return draft versions instead of published. |
--deleted |
Return recently deleted styles instead of active ones. |
Paginated the same way as tokens: the Link header carries the next
start.
mapbox styles list --username user
mapbox styles list --username user --limit 5
mapbox styles list --username user --id ckstyle00000000000000001aTerminal — -o text | Agent — -o json |
|---|---|
|
[{"created":"2025-09-04T12:06:35.359Z","id":"ckstyle00000000000000001a","modified":"2025-09-04T12:08:57.099Z","name":"Parks & Railways Highlight","owner":"user","protected":false,"visibility":"private"}] |
The id copies straight into the next command, which is the reason to look at a listing at all:
mapbox styles get ckstyle00000000000000001a --username userOne style document, conforming to the Mapbox Style Specification.
| Parameter | Effect |
|---|---|
--download |
Ask for it as a file attachment. |
--optimize |
Rewrite vector source URLs for cache optimization. |
mapbox styles get ckstyle00000000000000001a --username user
mapbox styles get ckstyle00000000000000001a --username user -o json > style.jsonstyles/v1 read — this, styles draft get, styles list, get-sprite-json
— is served through CloudFront with max-age=900, so a styles get right
after an styles update may still show the old document. The mutation
commands return the new state themselves, from the origin; trust that rather
than reading back. list-files --fresh true is the one read that rebuilds
instead of being served from cache.
A style document is a nested tree of layers and sources, not rows, so it
stays JSON in both modes — indented under text, one line under json.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"created":"2025-09-04T12:06:35.359Z","id":"ckstyle00000000000000001a","layers":[{"id":"background","paint":{"background-color":"#f8f9fa"},"type":"background"}],"version":8} |
Adds a new style. The server fills in created, id, modified, owner
and the sprite URL; name defaults to the style id if omitted.
--data/-d carries the style document. It must be valid against the
Mapbox Style Specification — anything else is a 422. The minimum the API
accepts is a name, a version, and empty sources and layers.
mapbox styles create --username user \
-d '{"name":"My Style","version":8,"sources":{},"layers":[]}'
mapbox styles create --username user -d "$(cat style.json)"The created style, as a single object, so a field list. Empty layers
renders as (none).
Terminal — -o text | Agent — -o json |
|---|---|
|
{"created":"2026-09-01T19:59:13.073Z","draft":false,"glyphs":"mapbox://fonts/mapbox/{fontstack}/{range}.pbf","id":"ckstyle00000000000000004d","layers":[],"modified":"2026-09-01T19:59:13.073Z","name":"My Style","owner":"user","protected":false,"sources":{},"sprite":"mapbox://sprites/user/ckstyle00000000000000004d/…","version":8,"visibility":"private"} |
The id is what every other styles command wants. A body the API cannot
read is a 400:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_400","message":"Invalid JSON received","status":400} |
Modifies an existing style. name is required in the body. Strip created
and modified before sending — including them is a 422. Cross-version
updates (v7 to v8) are rejected.
mapbox styles update ckstyle00000000000000004d --username user \
-d '{"name":"Renamed","version":8,"sources":{},"layers":[]}'The updated style, in the same shape styles create returns. modified
moves; created does not.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"created":"2026-09-01T19:59:13.073Z","draft":false,"id":"ckstyle00000000000000004d","layers":[],"modified":"2026-09-01T19:59:28.868Z","name":"Renamed",…} |
Note that the body replaces the style: the layers and sources you send
are the ones it will have, so read it with styles get first unless you mean
to empty it.
Removes a style and its sprites. Mapbox keeps a deleted style recoverable
for 30 days, but this CLI does not ship the operation that restores one —
see the Breaking entry in the changelog.
mapbox styles delete ckstyle00000000000000003c --username userA 204 carries no body. Rather than print nothing, the CLI confirms what happened — nothing is asked beforehand, so the line after the fact is the only acknowledgement there is.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"command":"styles delete","ok":true,"status":204} |
The style leaves the account immediately, and stays reachable for 30 days
through --deleted:
$ mapbox styles get ckstyle00000000000000003c --username user
Error: Style not found (HTTP 404)
$ mapbox styles list --username user --deleted
ID NAME CREATED DELETED MODIFIED OWNER PROTECTED VERSION VISIBILITY
ckstyle00000000000000003c Old ske… 2018-03… 2026-09… 2018-03… user no 8 publicTwo things that are easy to get wrong:
- Deleting an already-deleted style succeeds again. It answers 204 and
says
Deleted …a second time. Only an id that never existed is a 404. - The 404 below therefore means "no such style, ever" — not "already gone".
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_404","message":"Style not found","status":404} |
Unlike styles draft delete, which cannot tell a real id from a typo, this
one does.
The draft version of a style. Every style carries a published version and a draft; Mapbox Studio always edits the draft. With no draft, the published style comes back instead — which is also how you can tell a draft was discarded.
mapbox styles draft get ckstyle00000000000000004d --username userThe same shape as styles get, with "draft": true while a draft exists.
styles get.
Updates the draft. Creates one from the published version if none exists. The published style is untouched until it is published in Studio.
mapbox styles draft update ckstyle00000000000000004d --username user \
-d '{"name":"Work in progress","version":8,"sources":{},"layers":[]}'The updated draft. Two fields separate it from styles update's answer:
draft is yes/true, and the sprite URL ends in /draft.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"created":"2026-09-01T19:59:13.073Z","draft":true,"id":"ckstyle00000000000000004d","modified":"2026-09-01T19:59:29.344Z","name":"Work in progress","sprite":"mapbox://sprites/user/ckstyle00000000000000004d/draft",…} |
Discards the draft, reverting the style to its published version. The published style is untouched.
mapbox styles draft delete ckstyle00000000000000004d --username userTerminal — -o text | Agent — -o json |
|---|---|
|
{"command":"styles draft delete","ok":true,"status":204} |
It does what it says: after the delete, styles draft get returns the
published style with "draft": false. Confirmed on a style whose draft URL
had never been fetched, so no cached copy could stand in for the answer.
zzznosuchstylexyz000000000, which says Deleted zzznosuchstylexyz000000000.
and exits 0, while styles delete on the same id returns 404. The
confirmation is not evidence the style existed, and there is no prompt
beforehand.
Tiles by tileset id, plus the raster-array and vector-tile lookups that key
off one: raster from the Raster Tiles API, vector from the Vector Tiles
API, MRT tiles from a raster-array job, and query from the Tilequery
API. Four operations, out of four different specs, under one command
group (#116) — a caller asking about a tileset is asking the same kind of
question regardless of which API answers it.
Not to be confused with mapbox tilesets-cli, which
forwards to the separately installed Python Tilesets CLI and shares
nothing with this but the word.
The three tile commands return bytes, so --output does not apply on
them — redirect to a file. query returns GeoJSON.
One raster tile at standard resolution, 256×256.
<tilesets> may be several ids separated by commas, composited into one
tile. <z> <x> <y> are the tile coordinates.
<format> is png, pngraw, jpg, jpeg or webp. <quality> is
appended to it rather than being a separate URL segment, so jpg with 70
fetches .jpg70; pass "" for the format's default. Both are positional,
in that order.
mapbox tilesets get-tile mapbox.satellite 2 1 1 png "" > tile.png
mapbox tilesets get-tile mapbox.satellite 12 2048 1361 jpg 70 > tile.jpg70Image bytes; redirect to a file. The refusal to print them names an
extension matching what the API sent, which is not always what was asked
for — mapbox.satellite is stored as JPEG, so a png request still
suggests out.jpg.
| Terminal — refuses | Redirected — raw bytes |
|---|---|
|
|
One vector tile from one or more Mapbox-hosted tilesets. Up to 15 ids, comma-separated, composited into a single tile.
<format> is mvt or vector.pbf — the same bytes under two names.
--style <owner>/<style-id>[@<timestamp>] asks the API to filter the tile
to what that style actually draws.
mapbox tilesets get-mvt mapbox.mapbox-streets-v8 12 2048 1361 mvt > tile.mvt
mapbox tilesets get-mvt mapbox.mapbox-streets-v8,mapbox.mapbox-terrain-v2 \
12 2048 1361 mvt > composite.mvtProtobuf bytes; redirect to a file.
| Terminal — refuses | Redirected — raw bytes |
|---|---|
|
|
One MRT tile from a raster-array job.
--jobid <jobid> is required and comes from the raster-array job that
produced the tiles.
mapbox tilesets get-mrt --jobid <jobid> <tileset-id> 12 2048 1361 > tile.mrtNot exercised: this account has no raster-array job. A --jobid that does
not exist gets a 500 rather than a 404, so the error says nothing useful
about what was wrong:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_500","message":"Internal Server Error","status":500} |
What features a vector tileset has at or near a point.
query is a hand-picked name, not the one this command would otherwise
carry: tilequery's own spec spells its one operation's operationId after
its own URL path (getV4TilesetsTilequeryLonLatJson) rather than after
what it does, and that spec is not this repo's to rename. The name comes
from the maintainer-only decision record instead.
Neither older spelling still runs. get-v4tilesets-tilequery-lon-lat-json,
tilequery get-tilequery and tilequery get were each replaced in turn,
the last one folding the whole command into this group (#116) — see the
Breaking entry in the changelog.
<tilesets> may be several ids separated by commas. <lon> and <lat> are
the point.
| Parameter | Effect |
|---|---|
--radius <m> |
How far around the point to look. 0 means exactly there. |
--limit <n> |
How many features. Default 5, max 50. |
--layers <names> |
Only these source layers. |
--geometry <type> |
Only point, linestring or polygon. |
--dedupe |
Drop duplicate features. |
--bands <names> |
Raster-array bands to sample. |
--language / --worldview |
As in geocoding. |
mapbox tilesets query \
mapbox.mapbox-streets-v8 24.94 60.16 --limit 2
mapbox tilesets query \
mapbox.mapbox-streets-v8 -74.0 40.7 --radius 100 --layers buildingGeoJSON — a FeatureCollection of what was found. Each feature carries a
tilequery property saying how far away it was and which layer it came
from; under -o text those render as a numbered list, same shape as
geocoder's. properties.name is the label where a layer has one (a POI,
a place, a named road); where it doesn't, a properties.type the tileset
sent as a string stands in. Only some tilesets provide one — a
raster-array result has neither — so an entry with no name is a normal
answer, not a broken one.
What else a tileset puts in properties is its own business, and what the
lines above did not use is shown rather than dropped: every other property,
and everything in the tilequery object past layer and distance, gets
its own key: value line under the coordinates, in alphabetical order.
Used is the test, not the name — a POI carrying both name and type shows
type as an attribute, since name is what became its label.
The tilequery object's own values come in under dotted keys —
tilequery.band, tilequery.zoom — so that a tileset free to carry a
top-level zoom or geometry of its own keeps both.
A list of scalars goes on one line, comma-separated. What has structure
inside it does not fit a line at all and is left out: an object, or a list
with objects or lists in it. -o json is a flag away for those.
A vector tileset — height is one of the building layer's attributes, and
reaches the text column as its own line:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"features":[{"geometry":{"coordinates":[24.94,60.16],"type":"Point"},"properties":{"height":6.2,"tilequery":{"distance":0,"layer":"building"},"type":"building"},"type":"Feature"}],"type":"FeatureCollection"} |
A raster-array tileset queried with --bands answers with a different
shape: the sampled value under val — one number per band, so a list — and
a tilequery object naming the band, the zoom it was read at and the units,
with no type to stand in for a name and no distance. This one is the
spec's own rasterarray response example rather than a capture; this
account has no raster-array tileset to query.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"features":[{"geometry":{"coordinates":[-122.459,37.7754],"type":"Point"},"id":null,"properties":{"tilequery":{"band":"frame-0","layer":"data","units":"m","zoom":6},"val":[1.23]},"type":"Feature"}],"type":"FeatureCollection"} |
Installs the Mapbox Agent Skills — hand-written domain guidance for coding agents, covering cartography, token security, style quality, geospatial operations and the mobile and web SDKs. Twenty skills, each a directory of Markdown.
Not the same as generate-skills, which is its
neighbour in --help and writes something else entirely: that one renders a
skill describing this CLI's own commands, from the specs compiled into the
binary. These are about using Mapbox; that one is about using mapbox. They
share the destination flags below and nothing else.
Neither command needs a token — the skills repository is public.
/plugin marketplace add mapbox/mapbox-agent-skills and
npx skills add mapbox/mapbox-agent-skills install the same content and are
not going away. This exists for the environments where npm is unavailable or
unapproved but a signed mapbox binary is, and so that people who install the
CLI find out the skills exist at all.
The destination flags are shared with generate-skills and behave
identically:
| Parameter | Effect |
|---|---|
--agent <AGENT> |
Repeatable. Defaults to whichever agents are installed. |
--global |
Write to the agent's home directory rather than this project. |
--dir <DIR> |
Write here and nowhere else. Conflicts with --agent and --global. |
--ref <REF> |
Branch, tag or commit to install from. Defaults to main; a commit SHA pins the install. |
Fifteen agents are known, and at project level most of them read the same directory:
| Agent | --agent |
This project | Home |
|---|---|---|---|
| Claude Code | claude-code |
.claude/skills |
~/.claude/skills |
| Codex | codex |
.agents/skills |
~/.codex/skills |
| Amp | amp |
.agents/skills |
$XDG_CONFIG_HOME/agents/skills |
| Cline | cline |
.agents/skills |
~/.agents/skills |
| Continue | continue |
.continue/skills |
~/.continue/skills |
| Cursor | cursor |
.agents/skills |
~/.cursor/skills |
| Gemini CLI | gemini-cli |
.agents/skills |
~/.gemini/skills |
| GitHub Copilot | github-copilot |
.agents/skills |
~/.copilot/skills |
| Goose | goose |
.goose/skills |
$XDG_CONFIG_HOME/goose/skills |
| Kiro CLI | kiro-cli |
.kiro/skills |
~/.kiro/skills |
| OpenCode | opencode |
.agents/skills |
$XDG_CONFIG_HOME/opencode/skills |
| Qwen Code | qwen-code |
.qwen/skills |
~/.qwen/skills |
| Roo Code | roo |
.roo/skills |
~/.roo/skills |
| Windsurf | windsurf |
.windsurf/skills |
~/.codeium/windsurf/skills |
| Zed | zed |
.agents/skills |
~/.agents/skills |
.agents/skills is the converging cross-tool convention; Claude Code is the
holdout. Agents that share a directory are one write, not several, so
--agent codex --agent cursor --agent zed installs once.
CLAUDE_CONFIG_DIR and CODEX_HOME relocate those two agents' directories,
skills included. $XDG_CONFIG_HOME falls back to ~/.config on every
platform, macOS included, because that is where those agents look. An agent
not in the table is what --dir is for.
An agent counts as installed when its home directory exists — the presence of
somewhere to read a skill from, not a PATH lookup.
The published skills, with the first part of each description, and a * on
the ones already installed at the destinations that apply.
The same destination flags as install — --agent <AGENT>, --global,
--dir <DIR> — and --ref <REF>, all described above.
Here they decide only which destinations the * is checked against.
Unlike install, this does not need a destination to exist: it lists what is
published even on a machine with no agent on it.
mapbox agent-skills list
mapbox agent-skills list --agent claude-code
mapbox agent-skills list --ref v1.0.0text | json |
|---|---|
|
{
"ref": "main",
"repository": "mapbox/mapbox-agent-skills",
"skills": [
{
"name": "mapbox-cartography",
"description": "Expert guidance on map design principles…",
"installed": [".claude/skills"]
}
]
} |
The text column cuts each description to the line width; json carries them
whole.
Installs skills into every destination that applies. With no NAME, installs
all twenty.
| Parameter | Effect |
|---|---|
NAME |
Skill to install, repeatable. Defaults to all of them. An unknown name is an error that suggests the nearest published one. |
--force |
Replace a skill directory that is already there. |
--dry-run |
List the files it would write, then exit without writing them. |
Plus the destination flags and --ref above.
An existing skill directory stops the install — every conflict, across
every destination, is reported before anything is written. It is not a merge
and not an overwrite, because the directory may hold edits. --force replaces
it wholesale.
Each skill is written to a staging directory inside the destination and
renamed into place, so an interrupted install leaves either the old directory
or the complete new one. Upstream's evals/ directory is test tooling and is
never installed. An archive entry that is not a regular file is skipped, and
one whose path would escape the destination stops the extraction outright.
mapbox agent-skills install
mapbox agent-skills install mapbox-cartography mapbox-token-security
mapbox agent-skills install --agent claude-code --global
mapbox agent-skills install --dir ./skills --ref v1.0.0
mapbox agent-skills install --dry-runtext | json |
|---|---|
|
{
"dry_run": false,
"ref": "main",
"repository": "mapbox/mapbox-agent-skills",
"skills": ["mapbox-token-security", "mapbox-cartography"],
"destinations": [
{
"root": ".claude/skills",
"source": "Claude Code, this project",
"files": [".claude/skills/mapbox-cartography/SKILL.md"]
}
]
} |
--dry-run opens with Dry run — nothing was written. and sets
"dry_run": true; everything else is the same, including the file list.
A conflict is already_installed, and names each directory in the way:
Error: These skills are already installed:
./out/mapbox-cartography
Fix: Pass --force to replace them, or name only the skills you want.
Next: mapbox agent-skills install --force
Re-installs the skills already here and reports what moved. With no NAME,
every skill that is installed at the destinations that apply.
It never installs a skill that is not already there — that is install's
job, and an update that quietly added twenty directories because upstream
published them would be a different command. Naming one that is not installed
is an error suggesting install.
| Parameter | Effect |
|---|---|
NAME |
Skill to update, repeatable. Defaults to every skill already here. |
--dry-run |
Report what would change, then exit without changing it. |
Plus the destination flags — --agent <AGENT>, --global, --dir <DIR> —
and --ref <REF>, all above. There is no --force:
updating in place is the whole job.
The installed files are compared with the published ones byte for byte,
in both directions: a file edited upstream, a file added, and a file left
behind that upstream no longer publishes all count. So does a local edit,
which update restores — that is what updating means.
Because it cannot tell an upstream change from one of yours, it asks before
replacing anything, at a terminal, the same [y/N] question every
destructive command asks; --yes/MAPBOX_YES answers it in advance and
--dry-run reports without asking. A skill installed for two agents is one
skill: the counts and the updated/unchanged arrays are per skill, and the
destinations say where it went.
There is no lock file and no recorded tree SHA. Upstream's npx skills keeps
one so that update can avoid downloading an unchanged skill; this command
has already downloaded every skill in one tarball before it could consult any
record, so there is nothing left to save, and comparing the bytes answers
exactly rather than approximately. Nothing this command writes has to be kept
in step with another tool's file.
mapbox agent-skills update
mapbox agent-skills update mapbox-cartography
mapbox agent-skills update --dry-run
mapbox agent-skills update --ref v1.1.0text | json |
|---|---|
|
{
"dry_run": false,
"ref": "main",
"repository": "mapbox/mapbox-agent-skills",
"updated": ["mapbox-cartography"],
"unchanged": ["mapbox-token-security"],
"destinations": [
{
"root": ".claude/skills",
"source": "Claude Code, this project",
"skills": ["mapbox-cartography"]
}
]
} |
With nothing to do it says Everything is up to date with mapbox/mapbox-agent-skills@main. and "updated": [].
Removes installed skill directories. At least one NAME is required — an
empty list is not a licence to remove everything.
This is the one subcommand that makes no request. It works from what is on disk, which also means it can remove a skill that has since been unpublished.
| Parameter | Effect |
|---|---|
NAME |
Skill to remove, repeatable. Required. |
--force |
Remove a directory even though it holds no SKILL.md. |
--dry-run |
List what it would remove, then exit without removing it. |
Plus the destination flags — --agent <AGENT>, --global, --dir <DIR> —
above, which decide where it looks. There is no --ref:
nothing is fetched.
There is no manifest, so this cannot know that this command installed a
given directory (see update for why there is
no manifest). Three things stand in for one:
- a
NAMEhas to be one directory name..., an absolute path anda/bare refused outright,--forceincluded — this command removes directories, andPath::joinon an absolute path replaces the destination rather than extending it; - a directory holding no
SKILL.mdis refused until--force, since it is not shaped like an installed skill; - at a terminal it asks before deleting, the same
[y/N]question every destructive command asks —--yes/MAPBOX_YESanswers it in advance; --dry-runlists the directories first.
A skill installed by npx skills looks exactly like one installed here, and
removing it will leave that tool's .agents/.skill-lock.json describing a
skill that is gone. Nothing here writes or repairs that file.
mapbox agent-skills uninstall mapbox-cartography
mapbox agent-skills uninstall mapbox-cartography mapbox-token-security
mapbox agent-skills uninstall mapbox-cartography --dry-run
mapbox agent-skills uninstall mapbox-cartography --global --agent claude-codetext | json |
|---|---|
|
{
"dry_run": false,
"removed": [".claude/skills/mapbox-cartography"]
} |
--dry-run opens with Dry run — nothing was removed. and sets
"dry_run": true. A name that is not installed is not_installed and names
the directories it looked in.
Prints a completion script for one shell on stdout. Makes no request, needs no token, and writes nothing to disk.
The script is generated from the command tree this binary built, so it
completes exactly the commands this build has — a command group a spec sync added
is in it, and the withheld operations in the Not shipped column
above are absent from it for the same reason they are absent
from --help. Nothing about it is maintained by hand, and it cannot fall
behind the binary that printed it.
Commands, subcommands and flag names are completed. Values are not — completing a style id, a tileset id or a username would mean an API request and a live token in the middle of a keystroke.
--output/-o does not apply: the script is the result, and an envelope
around it would leave it unsourceable. Passing it explicitly earns a warning
on stderr; the ordinary > file redirect does not, and an exported
MAPBOX_OUTPUT does not.
| Parameter | Effect |
|---|---|
<SHELL> |
Required. One of bash, zsh, fish, powershell. |
Where each shell looks differs by machine — these are the common places, not the only ones:
# bash: any file bash-completion loads
mapbox completion bash > ~/.local/share/bash-completion/completions/mapbox
# zsh: any directory on $fpath, and the filename has to be `_mapbox`
mapbox completion zsh > ~/.zfunc/_mapbox
# fish
mapbox completion fish > ~/.config/fish/completions/mapbox.fish
# for the current shell only, no file
source <(mapbox completion bash)# PowerShell: append to the profile, which is what $PROFILE names
mapbox completion powershell | Out-String | Invoke-Expression
mapbox completion powershell >> $PROFILEThe same script in every output mode. The first lines of two of them:
mapbox completion bash | mapbox completion fish |
|---|---|
|
|
A maintainer-only test harness checks these against the real shells: it sources each script in the shell it names, and completes a command group, an operation and a flag in the two that can be driven without a terminal.
Writes this CLI's own command surface out as an Agent Skill: a mapbox-cli/
directory holding SKILL.md, an AGENTS.md carrying the same prose, and one
references/<service>.md per command group. Makes no request and needs no token.
The content is rendered from the command tree this binary built, so it
describes exactly the commands that exist — the operations in the Not
shipped column above, and the five liveness probes with
them, are absent from the skill for the same reason they are absent from
--help.
| Parameter | Effect |
|---|---|
--agent <AGENT> |
claude-code or codex, repeatable. Defaults to every agent whose home directory is present. |
--global |
Write under the agent's home directory rather than this project. |
--dir <DIR> |
Write here and nowhere else. Conflicts with --agent and --global. |
--service <SERVICE> |
Describe only this command group, repeatable. Defaults to all of them. |
--force |
Replace a skill directory holding files this command did not write. |
--dry-run |
List the files it would write, then exit without writing them. |
CLAUDE_CONFIG_DIR, CODEX_HOME |
Where each agent's home directory is, when it is not ~/.claude / ~/.codex. |
Without --global, a destination is relative to the current directory:
.claude/skills for Claude Code, .agents/skills for Codex.
mapbox generate-skills
mapbox generate-skills --agent claude-code --global
mapbox generate-skills --dry-run --dir ./out --service geocodertext | json |
|---|---|
|
{
"dry_run": true,
"skill": "mapbox-cli",
"destinations": [
{
"root": "./out/mapbox-cli",
"source": null,
"files": [
"SKILL.md",
"AGENTS.md",
"references/geocoder.md"
]
}
]
} |
Without --dry-run the first line reads Wrote 3 files under …, and source
names what chose the destination (Claude Code, this project) for anything
but --dir.
The directory is replaced whole rather than merged into. Every file carries a
Generated by `mapbox generate-skills` marker in its first kibibyte, and a
directory holding anything without one is refused — with the paths named —
until --force.
Removes the mapbox binary this process is running from — std::env::current_exe(),
not a path guessed from PATH or scripts/install.sh's default. Nothing
else: stored credentials, other profiles' lock files, and the separately
installed tilesets binary all survive. Run mapbox auth logout first if
the credential store should go too.
At a terminal it asks before deleting, the same [y/N] --yes /
MAPBOX_YES question every other destructive command asks; piped or
scripted, it proceeds without asking, like everything else in this CLI.
On Unix the file is gone by the time the command returns — unlinking a binary that is still executing is allowed; the process keeps its open handle until it exits. Windows locks a running executable's file, so there the deletion happens in a detached helper after this process exits, and the message says so rather than claiming the file is already gone.
| Parameter | Effect |
|---|---|
--dry-run |
Describe what this would delete, then exit without deleting it. |
mapbox uninstall
mapbox uninstall --yes
mapbox uninstall --dry-runtext | json |
|---|---|
|
{
"path": "/home/user/.local/bin/mapbox",
"removed": true
} |
--dry-run prints Dry run — nothing was changed.\nWould delete … and
{ "dry_run": true, "path": … } instead.
Usage per Mapbox product, by day, for the account or one token. Calls the
Statistics API (GET /statistics/v1), which needs the statistics:read
scope. mapbox auth login requests it by default now; log in again if
your stored token predates that. Also takes --token-id <id> for one
token's usage instead of the whole account — see mapbox accounts list-tokens for ids.
| Parameter | Effect |
|---|---|
--period-start <YYYY-MM-DD> |
Start of the usage period, inclusive. Defaults to 30 days ago. |
--period-end <YYYY-MM-DD> |
End of the usage period, inclusive. Defaults to today. At most 31 days after --period-start. |
--product <name> |
Only show this product. Matches the API's own name for it case-insensitively, exactly or as a substring (e.g. "search box" matches "Search Box API - Requests") — run without it first to see which names had usage. Client-side: the API has no such filter itself. |
--daily |
List each product's usage day by day instead of a sparkline. Only changes -o text; -o json always has the daily figures. |
mapbox usage
mapbox usage --period-start 2026-01-01 --period-end 2026-01-31
mapbox usage --product "Vector Tiles API"
mapbox usage --product "Vector Tiles API" --dailyRun live. Numbers below are made up — the real response carries actual traffic
figures, which do not belong in a page committed to the repo — but the
shape, including the sort order (busiest product first), the sparkline, and
every line -o text prints around the table, is exactly what came back.
Terminal — -o text | Agent — -o json |
|---|---|
|
{
"data": {
"period": { "start": "2026-08-09", "end": "2026-09-08" },
"token_id": null,
"activeDays": ["2026-08-09", "…", "2026-09-08"],
"products": {
"Directions API": {
"daily": [{ "date": "2026-08-09", "usage": 1993852 }, "…"],
"dimensions": { "browsers": ["…"], "countries": ["…"], "hosts": ["…"] }
},
"Matrix API": { "daily": ["…"], "dimensions": { "…": "…" } },
"Vector Tiles API": { "daily": ["…"], "dimensions": { "…": "…" } }
}
},
"generated_at": "2026-09-08T09:49:10.827Z"
} |
-o text's table is this CLI's own summary (render_text in
src/account_usage.rs), not render_human in src/output.rs: the response
nests a daily array and a dimensions object under each product, too deep
for that generic renderer, which would fall back to the same pretty JSON
-o json prints. Each row is a product's total for the period plus a
sparkline of its daily values — a padded one: a day the API's daily array
leaves out (it omits a day rather than sending usage: 0 for it) still gets
its own zero-height glyph at the right position in the line, computed from
data.period's own start and end. Rows have a blank line between them —
without it, a sparkline's solid glyphs sitting flush against the next
row's read as cramped rather than dense, on an account with more than a
couple of products. Exact per-day numbers and the per-browser/country/host
breakdown are left to -o json; --product narrows the whole response,
both columns, to one product's row. A 403 covers two different causes the
API doesn't otherwise distinguish: the token missing statistics:read — a
login from before the scope was added, most likely, and fixed by logging in
again — or, less commonly, an account with no access to the Statistics API
at all, which needs Mapbox support. A 401 means the token itself is missing
or invalid.
--daily swaps every product's sparkline row for its own day-by-day
listing — same total, same period, newest day first, no DAILY TREND
column header (there is no single column to head), and each day
right-padded to the widest number in that product's own series rather than
across all products:
mapbox usage --product "Directions API" --daily
Usage · 2026-08-09 → 2026-09-08
Directions API — total 67,840,000
2026-09-08 2,185,000
…
2026-08-11 2,200,000
2026-08-10 2,150,000
2026-08-09 2,100,000
Active days: 31
Generated 2026-09-08T09:49:10.827Z
Tips:
`-o json` for the per-browser/country/host breakdown.
-o json is unaffected by --daily — the day-by-day figures are already
there under each product's daily array either way, which is why the tip
list drops the --daily suggestion once it's already in effect.
Forwards everything verbatim to the separately installed tilesets binary
(PyPI mapbox-tilesets). On Unix it execs, so this process is replaced and
never sees the child's output.
| Parameter | Effect |
|---|---|
| Everything after the subcommand | Forwarded to tilesets untouched, including flags. |
--token, -t (before it) |
Injected into the child's environment, never its argv. |
--use-login (before it) |
Use stored credentials instead of MAPBOX_ACCESS_TOKEN. |
--profile <name> (before it) |
Which stored credentials to use. |
MAPBOX_TILESETS_CLI |
Path to a tilesets that is not on PATH. |
Globals must come before tilesets-cli; written after it they are
forwarded to the child, which does not know them. That earns a warning.
--output/-o does not apply — output comes from tilesets. Passing it
explicitly earns a warning; an exported MAPBOX_OUTPUT does not.
mapbox tilesets-cli list user
mapbox --use-login --profile work tilesets-cli upload-source user my-source data.geojson.ldWhatever tilesets prints. It is not uniform, and it is not always JSON:
tilesets list — bare IDs | tilesets list -v — JSON Lines |
|---|---|
|
{"type":"vector","id":"user.city-boundaries",…}
{"type":"vector","id":"user.test-cli",…} |
So mapbox tilesets-cli list <user> | jq . fails, and -v is what you want.
-v output is JSON Lines, not an array — jq . works, jq '.[0]' does not.
The fix is --output json upstream in
mapbox/tilesets-cli; this CLI
does not translate the flag itself, by design, to avoid masking upstream's
own output contract.
Errors go to stderr in both modes, and a failure writes nothing to stdout — a redirected file is either a complete result or empty, never half of each.
Terminal — -o text | Agent — -o json |
|---|---|
|
{"body":{"error_code":"INVALID_TOKEN","message":"Not Authorized - Invalid Token"},"code":"http_401","docs":["https://docs.mapbox.com/api/search/geocoding-v6/","https://docs.mapbox.com/api/accounts/tokens/"],"fix":"The token was passed with `--token`, which outranks both MAPBOX_ACCESS_TOKEN and your login — so signing in again would change nothing. Check the token you passed, or drop the flag to use one of the others.","message":"Not Authorized - Invalid Token","next_actions":["mapbox auth whoami"],"status":401} |
A failure that is about the request rather than the credential names the command that answers it — here a style id that does not exist, and the listing the ids that do exist come from:
Terminal — -o text | Agent — -o json |
|---|---|
|
{"code":"http_404","docs":["https://docs.mapbox.com/api/maps/styles/"],"fix":"Nothing exists at that path. The id may be misspelled, or it may belong to an account other than the one this token is for.","message":"Style not found","next_actions":["mapbox styles list --username user","mapbox auth whoami"],"status":404} |
The error is the whole document — there is no {"error": …} wrapper, since
stderr carries nothing else machine-readable and stdout never carries a
failure. It always carries code and message. It carries status and
body when the failure came from an API response — body only when it says
something the message does not.
Three further fields carry what to do about it, each present only when there is something to say. An empty list is never sent: a missing key already answers "was this computed?".
| Field | Holds |
|---|---|
fix |
One line: why it failed, and what would make it work. |
next_actions |
Commands to run, and nothing else — no prose to strip before running one. |
docs |
The pages that bear on the failure: the command's own, plus the tokens page when it was the credential that was refused. |
request_id |
The response's request id, for quoting to Mapbox support. |
request_id is the one field whose two renderings differ on purpose. Under
-o json it is there on every failure that carried one, whatever the
status, because a caller logging failures wants it on all of them and a field
costs nothing to ignore. Under -o text it is printed for a 5xx only:
Error: Internal server error (HTTP 500)
Fix: The service failed rather than refusing the request. Retry, and check https://status.mapbox.com if it persists.
Request ID: 01JC8K3Q7V9XZ4M2 (quote this to Mapbox support)
That is the failure a person escalates, and the id is what lets support find the request in their logs. A 404 on a mistyped id is the reader's own to fix, so an id under it would be noise on the common case.
The id is whatever identified the response: x-request-id from a service
that sends one, and otherwise x-amz-cf-id, the CloudFront id every Mapbox
response carries. Quote it as printed — support can trace either.
mapbox agent-skills is the one command whose failures carry no
request_id, and deliberately: it fetches from GitHub, which identifies
requests with its own header that Mapbox support cannot look up.
The advice is keyed on the HTTP status, with the command filling in what only
it knows — and it is read off the parsed spec, so a suggestion can only name
a command that exists. A 404 names the listing its ids come from, and the
account the failed request used. A 400 or 422 names --schema, which prints
what the command accepts without spending a request. A 403 asks about the
scope and the account, since the token was accepted there. A 5xx says to
retry and where to check whether the platform is degraded. A 401 is the
exception to the keying: its answer depends on which of --token, the
environment and a stored login supplied the token that failed, so it is
written where that is known (src/auth.rs) rather than in the status table
(src/remedy.rs). The example above is the typed-flag case, and the advice
names the flag rather than offering a login that could not outrank it.
| Code | Raised when |
|---|---|
http_<status> |
The API answered non-2xx. Carries status and the response body. |
request_failed |
Transport failure — proxy, DNS, TLS. Never carries the URL, because the access token rides in its query string. |
The CLI honours HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY, and
needs no proxy configuration of its own. Two things that look like network
faults and are not:
HTTP_PROXYalone does not carry Mapbox requests. Every base URL ishttps, and that variable covershttpURLs only — so a request goes direct and a proxy-only network refuses it.HTTPS_PROXYorALL_PROXYis the one to set.- SOCKS is not supported.
ALL_PROXY=socks5://…fails rather than being ignored, withunsupported scheme socks5in the message.tests/proxy.rspins that wording, because a bare "the network failed" on a machine where every other tool works is the expensive version of this answer.
| request_timed_out | The request ran out of its time budget. Its own code because it is the one transport failure worth retrying or raising --timeout for. |
| missing_path_parameters | A {username}/{owner}/{account} placeholder went unresolved. |
| invalid_path_parameter | A path parameter was . or .., which would move the request to a different endpoint. Other URL syntax in a path parameter (/, ?, #, \) is percent-encoded rather than refused, so it names a segment instead of changing the URL's shape. |
| invalid_data | --data was not valid JSON, or a @<path>/@- body was empty. |
| invalid_file | A file could not be read: one named by --file, or one named by --data @<path>. Also a @<path> that is not valid UTF-8, which a JSON body has to be. |
| binary_response | The response was bytes and stdout is a terminal. Redirect it to a file. |
| missing_subcommand | A command group was named with no operation. |
| cancelled | A delete was declined at the confirmation prompt. Nothing was sent. |
| usage | Anything else clap rejected. Exit code 2, not 1. |
| error | Unclassified. |
Exit codes: 0 success, 1 runtime failure, 2 usage error.
--help and --version are exempt — clap renders both, in every mode.