Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ that may never merge. They are not releases and are not listed here.
that passes them now fails with an unknown-argument error; drop the
flags. `mapbox search forward` keeps them. (#76)

- `mapbox places <mapbox-id>...`, full place detail — hours, phone,
website, photos, address, coordinates, activity data — for one or more
`mapbox_id`s a Search Box API result already returned, up to 100 in one
call. Always uses the batch endpoint, even for a single id, so there is
one command rather than a choice between one id and a hand-typed JSON
array. Hand-authored into `custom-openapi/` since no upstream spec
exists yet. Places is Public Preview, with a 1000-records-per-account
monthly quota.

- `mapbox matrix`, travel time and/or distance between every pair in a set
of up to 25 coordinates in one call, for driving (with or without live
traffic), walking, or cycling. No subcommand: like `mapbox directions`
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,11 @@ mapbox styles <operation>
mapbox tilesets <operation>
```

`mapbox directions`, `mapbox isochrone`, `mapbox map-match`, and
`mapbox matrix` are the exceptions: each API has a single operation, so
there's a bare command with no subcommand at all, the same shape
`mapbox usage` already has, see [docs/commands.md](./docs/commands.md)
for their own parameters.
`mapbox directions`, `mapbox isochrone`, `mapbox map-match`,
`mapbox matrix`, and `mapbox places` are the exceptions: each API has a
single exposed operation, so there's a bare command with no subcommand at
all, the same shape `mapbox usage` already has, see
[docs/commands.md](./docs/commands.md) for their own parameters.

A command group is not the same thing as a spec file: which one an operation
belongs to is decided per operation. So `sprites` and `tilesets` are each
Expand Down
100 changes: 100 additions & 0 deletions custom-openapi/places/openapi/places.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
openapi: "3.0.0"
# `parse_spec` turns `info.description` below into this service's clap
# `long_about`, so it also reaches `mapbox places --help`, `--schema` and
# `generate-skills` output. Keep it to API prose only — the provenance
# below is for whoever edits this file, not for a CLI user:
#
# Hand-authored down to the parameters documented at
# docs.mapbox.com/api/search/places. See `custom-openapi/README.md` for
# how a file like this is wired in.
#
# One command, not two: the API has a single-id GET and a batch POST, and
# an earlier version of this file exposed both as `places get`/`places
# batch`. Reviewed as the wrong shape for any API with that pair — a
# caller should not have to choose between "one id" and "a JSON array of
# ids" written by hand. `batch` is the one that survives, renamed to the
# bare `places` command via `FLATTENED_SERVICES`, taking one or more ids
# as positional arguments (`VARIADIC_BODY_ARRAY` in `src/spec.rs`) and
# always using the batch endpoint — even for a single id — so there is one
# response shape and one code path. The GET operation this used to expose
# as `places get` is gone from here entirely, not just hidden: nothing
# reaches it.
info:
title: "Mapbox Places API"
description: >-
Full detail for one or more places — hours, phone, website, photos,
address, coordinates, activity data — by the `mapbox_id`s a Search Box
API result already returned. This API has no search or suggest of its
own; it only resolves ids something else found. Public Preview, with
a 1000-records-per-account monthly quota.
version: "0.0.0"
servers:
- url: https://api.mapbox.com
description: Places API
paths:
/places/v1/details/retrieve:
post:
operationId: batch
summary: Full detail for one or more places.
description: >-
Each `mapbox_id` comes from a Search Box API result (`search
forward`/`reverse`/`category`) — this API does not look places up
by name or location itself. Always sends a batch request, one id
or many, so a single id reads the same way, no `get`/`batch`
choice to make. Up to 100 ids in one call.
parameters:
- name: "access_token"
in: query
required: true
description: "Mapbox API Access Token"
schema:
type: string
minLength: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
required: ["ids"]
properties:
ids:
type: array
description: >-
One or more Mapbox IDs, from a Search Box API result,
up to 100.
items:
type: string
minLength: 1
minItems: 1
maxItems: 100
responses:
"200":
description: >-
Every id resolved: `{"results": [<place record>, ...]}`, one
entry per id — `name`, `full_address`, `primary_category`/
`categories`, `coordinates` (with `routable_points`),
structured `address`, `score` (`closed`/`reality`/
`popularity`, each 0-1), and where available `brand`,
`opening_hours`, `phone`, `photos`, `website`, `building`, and
`telemetry` (hourly activity by day of week).
"206":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exit 0 when some ids are missing makes sense: the caller still gets the ones that resolved. But when no id resolves, mapbox places <id> exits 0 with "results": [] and prints nothing to stderr. Before, places get failed with 404. Please exit non-zero in this case. Exit codes are part of the contract, so this is easier to decide before release.

Optional: also print the missing/unprocessed ids to stderr.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: when no id resolves at all, the CLI now exits non-zero instead of reporting success with an empty results. The ids that didn't resolve print to stderr too (took your optional suggestion). Tested against production with a well-formed but nonexistent id:

$ mapbox places <made-up-id>
No id resolved: <made-up-id>
Error: No id resolved. See the ids above, or re-check them against a Search Box result.
$ echo $?
1

Partial success (some ids resolve, some don't) still exits 0, unchanged.

description: >-
Partial Content — some ids didn't resolve. Same body as 200,
plus `missing` (ids with no such place) and/or `unprocessed`
(ids that couldn't be processed) alongside `results`. Treated
as success, the same as 200 — a caller still gets every id
that did resolve, and `missing`/`unprocessed` say which ones
didn't rather than failing the whole request over them.
"400":
description: >-
Bad Request — `ids` isn't a non-empty array of at most 100
entries. The 100 limit is also enforced client-side, before
the request goes out.
"401":
description: Unauthorized
"429":
description: >-
Too Many Requests — the per-second rate limit (charged by id
count) or the Public Preview's monthly quota (1000
records/account) was exceeded.
173 changes: 154 additions & 19 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Implemented commands

Every command the CLI ships, including five auth commands, 39 API operations (35 across 9
command groups, plus `directions`, `isochrone`, `map-match` and `matrix`), the
tilesets-cli proxy, `completion` and `generate-skills`. Each is
Every command the CLI ships, including five auth commands, 40 API operations (35 across 9
command groups, plus `directions`, `isochrone`, `map-match`, `matrix` and
`places`), 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
Expand All @@ -11,12 +11,12 @@ gets the right one. See
Account names, style ids and tokens in the examples are replaced; everything
else is as the API sent it.

**35 of the 39 were run against the live API and show what came back:** 24
**36 of the 40 were run against the live API and show what came back:** 24
on 2026-09-01, `styles download` by 2026-10-01, `fonts list`, `fonts upload` and `fonts delete` on
2026-09-08, once `fonts:list`/`fonts:write` became registrable,
`directions`, `isochrone` and `map-match` on 2026-09-23, `matrix`,
`feedback list` and `feedback get` on 2026-09-24, and `feedback create` on
2026-10-02.
`directions`, `isochrone` and `map-match` on 2026-09-23, `matrix` and
`feedback list`/`feedback get` on 2026-09-24, `feedback create` on
2026-10-02, and `places` on 2026-10-09.
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
Expand All @@ -34,9 +34,12 @@ commands and the flags they take — is held to `mapbox --schema` on every
can be checked cannot fall behind the binary.

The remaining 4 give the response shape from the spec or the docs instead
of a live capture: they're all `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.
of a live capture. All four are `search`'s, marked as no-access when this
page was first written — no longer true, discovered while writing
`places`'s example above, which needed a real `search forward` result to
test against and got one. `search`'s own four sections below haven't been
re-captured with this pass, since that's a different command group's page
to touch; a worthwhile follow-up, not done here.

Each **Parameters** section lists only what is specific to its command. The
globals every API command takes are
Expand Down Expand Up @@ -103,6 +106,8 @@ nests, and is typed `mapbox styles draft get`.

**[Matrix](#matrix)** — [matrix](#mapbox-matrix)

**[Places](#places)** — [places](#mapbox-places)

**[Search](#search)** — [search.forward](#mapbox-search-forward) ·
[search.reverse](#mapbox-search-reverse) ·
[search.category](#mapbox-search-category) ·
Expand Down Expand Up @@ -457,11 +462,11 @@ No stored profiles. Run `mapbox auth login` to create one.

## API command groups

35 operations across 9 command groups, plus four single-operation commands
with no group: `directions`, `isochrone`, `map-match` and `matrix`. Seven
groups are generated from the OpenAPI specs vendored in `openapi/`; `search`,
`feedback` and the four single commands are the exceptions — hand-authored
specs versioned in this repo's own `custom-openapi/`, see
35 operations across 9 command groups, plus five single-operation commands
with no group: `directions`, `isochrone`, `map-match`, `matrix` and `places`.
Seven groups are generated from the OpenAPI specs vendored in `openapi/`;
`search`, `feedback` and the five single commands are the exceptions —
hand-authored specs 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
Expand All @@ -485,7 +490,7 @@ moved to `tilesets` or `static`.
| [`styles`](#styles) | **9** |
| [`tilesets`](#tilesets) | **3** |

The [Contents](#contents) list above names every one of the 39.
The [Contents](#contents) list above names every one of the 40.

**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
Expand Down Expand Up @@ -593,8 +598,8 @@ Three things worth knowing about the read forms:

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` — 13 of
the 39 operations — plus `auth login`, `auth logout`, `auth refresh` and
a list anyone keeps: it is every `POST`, `PUT`, `PATCH` and `DELETE` — 14 of
the 40 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
Expand Down Expand Up @@ -1808,7 +1813,6 @@ is the authority on whether a value is valid, not this page.
| `--destinations <indices>` | Which coordinates are matrix columns — same rules as `--sources`. |
| `--fallback-speed <km/h>` | Replaces a `null` (unreachable) cell with a straight-line estimate at this speed, rather than leaving it `null`. Legacy. |
| `--depart-at <ISO 8601>` | For future traffic conditions and time-dependent road restrictions. |

#### Examples

```sh
Expand Down Expand Up @@ -1867,6 +1871,137 @@ Neither output mode has a bespoke rendering for this response, same as

---

## Places

Full detail for one or more places — hours, phone, website, photos,
address, coordinates, activity data — by the `mapbox_id`s a Search Box
API result already returned. Curated by hand down to the parameters
documented at docs.mapbox.com/api/search/places — see
`custom-openapi/README.md` for why this command group doesn't come from
the vendored specs the way most others do. Public Preview, with a
1000-records-per-account monthly quota.

**This API has no search or suggest of its own.** It only resolves ids
`search forward`/`reverse`/`category` already returned — the detail-view
follow-up to a search result, not a way to find places by name or location.

### `mapbox places`

One or more full place records. No subcommand: the API has a single-id
GET and a batch POST of the same thing, and `mapbox places` always sends
the batch request, one id or many, so there is one command and one
response shape whether given a single id or several.

#### Parameters

`<mapbox-id>...` (positional, one or more, up to 100) — each from a
Search Box API result. More than 100 is a usage error before the request
goes out.

#### Examples

```sh
mapbox places dXJuOm1ieHBvaTpmYTE5Y2NhMC0yZmQ3LTQwMzgtYTEzNy02MzFmNGEwZDI5ODA
mapbox places dXJuOm1ieHBvaTpmYTE5Y2NhMC0yZmQ3LTQwMzgtYTEzNy02MzFmNGEwZDI5ODA \
dXJuOm1ieHBvaTo1NTM4ZTE2OC0yMDM1LTRjODItODBkOC01NjY2YWM0MzhkYmE
```

#### Outputs

The response is always `{"results": [<place record>, ...]}`, one entry
per id, each carrying `name`, `full_address`, `primary_category`/
`categories`, `coordinates` (with `routable_points`), structured
`address`, `score` (`closed`/`reality`/`popularity`, each 0-1), and where
available `brand`, `opening_hours`, `phone`, `photos`, `website`,
`building`, and `telemetry` (hourly activity by day of week).

Captured live, a real place found via `search forward --q "Ferry Building
San Francisco"`:

<table>
<tr><th width="50%">Terminal — <code>-o text</code></th><th width="50%">Agent — <code>-o json</code></th></tr>
<tr><td>

```text
NAME BRAND CREATED_AT FULL_ADDRESS MAPBOX_ID OPENING_HOURS PERMANENTLY_CLOSED PHONE PRIMARY_CATEGORY STATUS UPDATED_AT WEBSITE
Ferry Building - 2026-07-0… San Francis… dXJuOm1i… Sa 08:00-14:… no +141523… food active 2026-10-0… http://…
```

</td><td>

```json
{"results":[{"address":{"city":"San Francisco","neighborhood":"Financial District","postcode":"94105","region":"California","region_code_full":"US-CA","country":"United States","country_code":"US"},"attributes":{"accommodation_wheelchair_accessible_entrance":true,"offering_coffee":true,"offering_dessert":true,"offering_lunch":true,"service_dine_in":true,"service_takeout":true},"brand":null,"categories":["cafe","food","food_and_drink"],"coordinates":{"latitude":37.79557765,"longitude":-122.39332918,"source":"poi","routable_points":[{"name":"driving","latitude":37.79559358806043,"longitude":-122.39333830635825}]},"created_at":"2026-07-02T02:56:22.965","full_address":"San Francisco, California, 94105, United States","mapbox_id":"dXJuOm1ieHBvaTpmYTE5Y2NhMC0yZmQ3LTQwMzgtYTEzNy02MzFmNGEwZDI5ODA","name":"Ferry Building","opening_hours":"Sa 08:00-14:00","permanently_closed":false,"phone":"+14152373318","primary_category":"food","score":{"closed":0,"reality":0.973,"popularity":0.275},"status":"active","updated_at":"2026-10-06T04:01:42.871","website":"http://crumbleandwhisk.com/"}]}
```

</td></tr>
</table>

Dropped most of `attributes` (14+ boolean amenity flags — wheelchair
access, payment types, and the like) for length; the real response
carries them all. `brand` is `null` here since this isn't a chain
location.

A second id adds a second row to the same table — captured live by
pairing the Ferry Building id above with a real Starbucks location
(`search forward --q "Starbucks" --proximity …`):

<table>
<tr><th width="50%">Terminal — <code>-o text</code></th><th width="50%">Agent — <code>-o json</code></th></tr>
<tr><td>

```text
NAME BRAND FULL_ADDRESS MAPBOX_ID OPENING_HOURS PHONE PRIMARY_CATEGORY WEBSITE
Ferry Building - San Francis… dXJuOm1i… Sa 08:00-14:… +141523… food http://…
Starbucks Starbuc… 7 Drumm Str… dXJuOm1i… Mo 05:30-13:… +134138… teahouse https:/…
```

</td><td>

```json
{"results":[{"mapbox_id":"dXJuOm1ieHBvaTpmYTE5Y2NhMC0yZmQ3LTQwMzgtYTEzNy02MzFmNGEwZDI5ODA","name":"Ferry Building","brand":null,"full_address":"San Francisco, California, 94105, United States","primary_category":"food","status":"active","opening_hours":"Sa 08:00-14:00","phone":"+14152373318","website":"http://crumbleandwhisk.com/"},{"mapbox_id":"dXJuOm1ieHBvaTo1NTM4ZTE2OC0yMDM1LTRjODItODBkOC01NjY2YWM0MzhkYmE","name":"Starbucks","brand":"Starbucks","full_address":"7 Drumm Street, San Francisco, California 94111, United States","primary_category":"teahouse","status":"active","opening_hours":"Mo 05:30-13:00; Tu 05:30-13:00; We 05:30-13:00; Th 05:30-13:00; Fr 05:30-13:00","phone":"+13413880001","website":"https://www.starbucks.com/store-locator/store/1013857/"}]}
```

</td></tr>
</table>

The response's own nested objects (`coordinates`, `address`, `score`,
`attributes`) and arrays (`categories`, `photos`) are dropped from the
table — only scalar fields become columns, the same rule every table on
this page follows — so the full detail is a `-o json` away. `CREATED_AT`,
`PERMANENTLY_CLOSED`, `STATUS` and `UPDATED_AT` drop out of the two-row
table above for a different reason: every row shares the same value for
each (this dataset's own coincidence, not something the API promises),
and a column that doesn't vary across rows is dropped as uninformative —
the same "varying" rule the rest of this page's tables follow.

`206` with `missing` and/or `unprocessed` alongside `results` — some ids
didn't resolve — is still exit 0: the caller got every id that did
resolve. Captured live by pairing the Ferry Building id above with a
well-formed id that doesn't exist:

```json
{"missing":["dXJuOm1ieHBvaTo2NTNkZjMzMS0zNTA2LTQ1MGEtYjg2Yi0wODY0ZmQ2NDRiYzA"],"results":[{"mapbox_id":"dXJuOm1ieHBvaTpmYTE5Y2NhMC0yZmQ3LTQwMzgtYTEzNy02MzFmNGEwZDI5ODA","name":"Ferry Building","full_address":"San Francisco, California, 94105, United States","primary_category":"food","status":"active","opening_hours":"Sa 08:00-14:00","phone":"+14152373318","website":"http://crumbleandwhisk.com/"}]}
```

Two keys at the top level here, not one, so `-o text` skips the usual
one-key-object-to-table unwrap and falls back to pretty-printed JSON —
the same response, read either way.

When *no* id resolves at all, the CLI exits non-zero instead of reporting
success with an empty `results` — the old `places get` answered that case
with a 404, and a silent exit 0 here would be a regression from that. The
ids that didn't resolve are printed to stderr:

```console
$ mapbox places dXJuOm1ieHBvaTo2NTNkZjMzMS0zNTA2LTQ1MGEtYjg2Yi0wODY0ZmQ2NDRiYzA
No id resolved: dXJuOm1ieHBvaTo2NTNkZjMzMS0zNTA2LTQ1MGEtYjg2Yi0wODY0ZmQ2NDRiYzA
Error: No id resolved. See the ids above, or re-check them against a Search Box result.
$ echo $?
1
```

---

## Search

The public, non-interactive surface of the Search Box API: text search,
Expand Down
Loading
Loading