This is the full developer documentation for Close API
# Quickstart
> From nothing to a walkability reading in five minutes — the first call needs no account, the second needs one free key.
This walks from an empty terminal to a real walkability reading. The first step runs with no account at all. The rest need one free key, which takes about a minute to get.
## 1. Look up a place (no key needed)
[Section titled “1. Look up a place (no key needed)”](#1-look-up-a-place-no-key-needed)
The catalog and place-lookup routes are free and need no key. Turn a city name into a GEOID and a centre point:
```bash
curl "https://api.close.city/v1/places?q=Providence&limit=1"
```
```json
{ "places": [
{ "name": "Providence", "geoid": "4459000",
"lon": -71.41872, "lat": 41.82301 }
] }
```
That centre point is the input to the metered routes below.
## 2. Get a free key
[Section titled “2. Get a free key”](#2-get-a-free-key)
Metered routes need an API key. Create one at [account.close.city](https://account.close.city): enter your email, click the link (or type the six-digit code back into the page), and open the dashboard. New accounts receive 5,000 free tokens, and no card is required. On the dashboard, choose **Create key** and copy the `ck_live_` key it shows once.
Set it as an environment variable so the examples below can read it:
```bash
export CLOSECITY_KEY="ck_live_your_key_here"
```
(Building an agent that signs up on a user’s behalf? See [For AI agents](/ai/) for the same flow over JSON.)
## 3. Read the walkability of a point
[Section titled “3. Read the walkability of a point”](#3-read-the-walkability-of-a-point)
Ask how long it takes to walk to each kind of amenity from that centre point:
```bash
curl -H "Authorization: Bearer $CLOSECITY_KEY" \
"https://api.close.city/v1/point/summary?lat=41.82301&lon=-71.41872&mode=walk"
```
```json
{ "resolved_block": "440070008003031",
"results": [
{ "dest_type_id": 1, "mode": "walk", "travel_time": 15.0 },
{ "dest_type_id": 7, "mode": "walk", "travel_time": 15.0 }
] }
```
Each row is the walk time in minutes to the nearest amenity of one type. Join `GET /v1/meta/destination-types` (free) to turn `dest_type_id` into a name. A missing type means it is not reachable on foot within 30 minutes — see [The data](/guide/data/). That call charged one token per returned row; watch the `X-Tokens-Charged` and `X-Tokens-Remaining` response headers to track spend.
## 4. Do it in Python or R
[Section titled “4. Do it in Python or R”](#4-do-it-in-python-or-r)
The SDKs return the same data as a `pandas`/`geopandas` frame (Python) or a data frame / `sf` object (R), ready to map. Both read `CLOSECITY_KEY` from the environment.
```python
pip install closecity
```
```python
from closecity import Client
close = Client() # reads CLOSECITY_KEY
here = close.places("Providence").iloc[0]
summary = close.point_summary(lat = here.lat, lon = here.lon, mode = "walk")
print(summary)
```
```r
install.packages("closecity") # or from source until it lands on CRAN
library(closecity)
close <- close_client() # reads CLOSECITY_KEY
here <- close$places("Providence")[1, ]
summary <- close$point_summary(lat = here$lat, lon = here$lon, mode = "walk")
print(summary)
```
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
* [The data](/guide/data/) — what a travel time means, and why absent is not zero.
* [Efficient token use](/guide/tips/) — what queries cost, in tokens and dollars.
* The [Python](/sdks/python/) and [R](/sdks/r/) SDKs each carry three worked tutorials that map a neighbourhood end to end.
# Why Close
> What you can build with block-level travel times, and how the data differs from a single walkability score.
Close publishes the travel-time matrix behind [close.city](https://close.city): for every US census block, how long it takes to reach the nearest amenity of each kind, on foot, by bike, and by transit. It is the raw material for walkability analysis, not a finished score.
## What you can build
[Section titled “What you can build”](#what-you-can-build)
* **Neighbourhood access profiles.** For any block or point, the walk, bike, and transit time to groceries, schools, parks, transit, healthcare, and more.
* **Coverage and equity analysis.** Which blocks can reach a full basket of amenities within fifteen minutes, and which cannot — joined to census demographics by GEOID.
* **Catchments and walksheds.** The blocks that can reach a given store, or the area reachable from a location within a time budget, as GeoJSON contours.
* **Site selection.** Rank candidate locations by what is reachable around them.
The SDKs carry three worked examples of this — an amenity basket, a competitor walkshed, and a home search — in both [Python](/sdks/python/) and [R](/sdks/r/).
## How this differs from a walkability score
[Section titled “How this differs from a walkability score”](#how-this-differs-from-a-walkability-score)
A walkability score gives one number per address. It is convenient, but it is opaque: you cannot see which amenities drove it, change the weighting, or join it to other data.
Close gives the measurements underneath instead — the actual travel time to each amenity type, per block. That means you can:
* **Choose what matters.** Weight groceries over gyms, or count only what is reachable by transit. The score is yours to define.
* **Reproduce it.** Every component carries a vintage (`GET /v1/meta/vintage`), so an analysis can be pinned to a data version and repeated. See [Citing Close](/guide/citing/).
* **Join it.** Block GEOIDs link directly to census demographics and boundaries.
* **Map it.** The SDKs return `geopandas` / `sf` objects ready to plot.
If you want a single readable score, you can still compute one — but you compute it from data you can inspect, rather than trusting a black box.
## Who it is for
[Section titled “Who it is for”](#who-it-is-for)
* **Researchers** studying access, equity, and the built environment, who need documented methods and a citable, versioned source.
* **Planners** measuring coverage and gaps across a city or county.
* **Data scientists** who want tidy travel-time frames in Python or R.
* **AI agents** doing spatial analysis on a user’s behalf — see [For AI agents](/ai/).
Start with the [Quickstart](/start/quickstart/).
# Introduction
> What the Close API is, its base URL, and the core concepts shared across every endpoint.
Travel times from every US census block to nearby points of interest, by walking, biking, and public transit — the data behind [close.city](https://close.city). Read-only, metered by returned rows.
**Base URL:** `https://api.close.city`
The machine-readable contract is the [OpenAPI reference](/reference/), generated from the same spec these docs are built on. Every response shape in the guide is verified against that spec in the API’s test suite.
## Core concepts
[Section titled “Core concepts”](#core-concepts)
* **Modes:** `walk`, `bike`, `transit`. (There is no drive mode.) Repeat the `mode` parameter to select several — `?mode=walk&mode=transit` — or omit it for all. The block and point routes return `mode` as a string label; the areal routes (`/v1/blocks/query`, `/v1/places/{geoid}/blocks`) return a numeric `mode_id`.
* **Destination types:** numeric ids from `GET /v1/meta/destination-types` (see the [full list](/guide/destination-types/)). Types are leaves (e.g. grocery stores) or parents (e.g. “Public schools”, covering elementary/middle/high). Filtering by a parent expands to its leaves.
* **Travel times** are in minutes and always capped at 30, matching close.city. A missing (block, type) row means “not reachable within 30 minutes”, not zero — see [The data](/guide/data/).
* **GEOIDs** are 15-digit census block identifiers; census-place GEOIDs identify cities and towns.
New here? [The data](/guide/data/) explains what Close measures and how, and the [Python](/sdks/python/) and [R](/sdks/r/) SDKs each have a get-started tour and worked tutorials that map the results at every step.
## Your first request
[Section titled “Your first request”](#your-first-request)
```bash
curl -H "Authorization: Bearer $CLOSECITY_KEY" \
"https://api.close.city/v1/blocks/410390020001010/summary?mode=walk"
```
```json
{
"block": { "geoid": "410390020001010", "population": 1187,
"land_area_m2": 183245.0 },
"results": [
{ "dest_type_id": 30, "mode": "walk", "travel_time": 6.5 }
]
}
```
Next: [authenticate](/guide/authentication/), then learn how [metering](/guide/metering/) and [pagination](/guide/pagination/) work.
# The data
> What Close measures, how travel times are computed, and the coverage and limits to know before you read the numbers.
Close answers one question for every census block in the United States: how long does it take to reach the nearest amenity of each kind, on foot, by bike, and by public transit?
## Census blocks
[Section titled “Census blocks”](#census-blocks)
A census block is the smallest area the US Census Bureau publishes, usually a single city block in a town and a larger polygon in rural areas. There are about 8.5 million of them, each with a 15-digit GEOID. Close computes travel times from every block, so a block GEOID is the unit you work with: the origin of a summary, a row in an areal query, the centre of an isochrone.
## What goes into a travel time
[Section titled “What goes into a travel time”](#what-goes-into-a-travel-time)
Travel times are routed from each block’s centre to the amenities around it. The pieces come from a few open datasets, and `GET /v1/meta/vintage` reports the live version of each:
| Component | Source | What it provides |
| ----------------- | ---------------------------- | ---------------------------------------- |
| `road_network` | OpenStreetMap | streets and paths for walking and biking |
| `transit_network` | GTFS feeds | scheduled public-transit service |
| `pois` | Open points-of-interest data | the amenities themselves |
| `blocks` | 2020 US Census | block boundaries and population |
| `parks` | OpenStreetMap parks | park access points, by size |
Times are door to door, including the walk to a stop and any waiting for transit. Transit times assume a weekday departure around 8am and report a good-day (10th-percentile) trip; every isochrone reply echoes these `assumptions`, and `GET /v1/isochrone/meta` lists them without spending a token.
## Two things that surprise people
[Section titled “Two things that surprise people”](#two-things-that-surprise-people)
**Times are capped at 30 minutes.** Close is about what is *nearby*: beyond half an hour, “how close is the nearest one” stops being the useful question. A time you read is always between 1 and 30 minutes.
**A missing row means “not reachable within 30 minutes”, not zero.** In an areal query, if a block has no row for grocery stores by walking, that block cannot walk to a grocery store inside the cap. This is the single most important rule for coverage analysis: absent is not the same as fast. When you count how many amenities a block can reach, the blocks with no rows for a category are the gaps.
## Coverage and limits
[Section titled “Coverage and limits”](#coverage-and-limits)
* The data covers the United States.
* Times are computed from block *centres*, so a very large amenity (a park, a campus) is treated as a single point. Contour maps fill small interior holes for the same reason; see [Isochrones](/guide/isochrones/).
* Each component has its own vintage. Check `GET /v1/meta/vintage` to see how current each one is, and `GET /v1/last-updated` for the newest publication time.
* Found a wrong or missing amenity? Report it from the [close.city](https://close.city) map so the correction reaches the next build.
# For AI agents
> Everything an LLM or coding agent needs to use the Close API — base URL, auth, programmatic sign-up, metering, and the footguns.
This page is the compact reference for using the Close API from an LLM or agent. Humans: the [Quickstart](/start/quickstart/) is friendlier.
## Basics
[Section titled “Basics”](#basics)
* **Base URL:** `https://api.close.city`
* **Auth:** `Authorization: Bearer `, where the key is a `ck_live_` API key.
* **Machine contract:** [`/openapi.json`](https://api.close.city/openapi.json) (OpenAPI 3.1, with a `servers` block and a bearer `securityScheme`). Also mirrored at [`docs.close.city/openapi.json`](https://docs.close.city/openapi.json).
* **Free, keyless routes:** `GET /v1/health`, `/v1/last-updated`, `/v1/meta/modes`, `/v1/meta/destination-types`, `/v1/meta/vintage`, `/v1/places`, `/v1/isochrone/meta`. Everything else needs a key.
## Metering
[Section titled “Metering”](#metering)
* 1 token per returned row, minimum 1 per request. Isochrones cost 10 tokens per contour instead. A `304 Not Modified` is free.
* Every metered reply carries `X-Tokens-Charged` and `X-Tokens-Remaining` — read them to track spend.
* Out of tokens returns `429` with the slug `tokens-exhausted`.
## Getting a key programmatically
[Section titled “Getting a key programmatically”](#getting-a-key-programmatically)
An agent can complete sign-up on a user’s behalf. The user must read a six-digit code from their own inbox — that human-in-the-loop step is deliberate. Keep a cookie jar across the calls:
```bash
# 1. Request a sign-in code for the user's email.
curl -s -X POST https://account.close.city/auth/request \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# 2. The user reads the six-digit code from their email and gives it to you.
# 3. Verify it; this sets the session cookie in the jar.
curl -s -c jar.txt -X POST https://account.close.city/auth/verify-code \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "code": "123456"}'
# 4. Read the CSRF token for the session.
CSRF=$(curl -s -b jar.txt https://account.close.city/auth/csrf \
| python3 -c "import json,sys; print(json.load(sys.stdin)['csrf'])")
# 5. Mint an API key (shown once).
curl -s -b jar.txt -X POST https://account.close.city/keys \
-H "Content-Type: application/json" -H "X-CSRF-Token: $CSRF" \
-d '{"label": "my-agent"}'
# -> {"key": "ck_live_...", "prefix": "ck_live_xxxx"}
```
The new account receives 5,000 free tokens. Store the key; it is shown only once.
## Errors
[Section titled “Errors”](#errors)
Errors are RFC 9457 `application/problem+json`. Match on the **slug** (the last path segment of `type`), never the human `title`:
```json
{ "type": "https://api.close.city/problems/invalid-mode",
"title": "Unknown mode 'drive'.", "status": 400,
"detail": "Valid modes: bike, transit, walk." }
```
Common slugs: `missing-key`, `invalid-key` (401); `tokens-exhausted`, `rate-limited` (429, with `Retry-After`); `invalid-parameters`, `invalid-cursor` (400); `block-not-found`, `no-isochrone-data` (404).
## Footguns
[Section titled “Footguns”](#footguns)
* **Modes are `walk`, `bike`, `transit` — there is no drive mode.**
* **GEOIDs are strings with leading zeros.** Do not coerce them to integers.
* **Areal routes return a numeric `mode_id`, not the string `mode`.** Join `/v1/meta/modes` to label it. The block and point routes return the string.
* **A missing (block, type) row means “not reachable within 30 minutes”, not zero.** See [The data](/guide/data/).
* **Pagination is by opaque cursor.** Follow `next_cursor` until it is null. The Python SDK does this automatically; the R SDK does too, unless you pass `paginate = FALSE`.
* **Neither SDK retries.** On `429`/`5xx`, back off using `Retry-After` yourself.
## MCP server
[Section titled “MCP server”](#mcp-server)
Close runs a remote [Model Context Protocol](https://modelcontextprotocol.io) server at `https://mcp.close.city/mcp`, so agents can call the API as tools instead of writing HTTP requests. Tools: `find_place` and `get_catalog` (free), `walkability_summary`, `nearby_pois`, `isochrone`, `blocks_in_area` (metered), and `request_sign_in` / `redeem_code` to get the user a free key in-chat.
Add it in Claude Code:
```bash
claude mcp add --transport http close https://mcp.close.city/mcp \
--header "Authorization: Bearer ck_live_your_key"
```
Or in a client that reads a JSON config (Cursor, Claude Desktop):
```json
{
"mcpServers": {
"close": {
"type": "http",
"url": "https://mcp.close.city/mcp",
"headers": { "Authorization": "Bearer ck_live_your_key" }
}
}
}
```
Without the key header, the free tools still work and the metered ones tell the agent to sign up.
## Machine-readable files
[Section titled “Machine-readable files”](#machine-readable-files)
* [`/openapi.json`](https://api.close.city/openapi.json) — the full contract.
* [`/llms.txt`](https://docs.close.city/llms.txt) — this documentation, indexed for LLMs, with [`/llms-full.txt`](https://docs.close.city/llms-full.txt) for the complete text.
# Authentication
> How API keys work — creating an account, bearer tokens, key management, and which routes are free.
Every metered route needs an API key, sent as a bearer token:
```plaintext
Authorization: Bearer ck_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
## Create an account and a key
[Section titled “Create an account and a key”](#create-an-account-and-a-key)
1. Go to [account.close.city](https://account.close.city) and enter your email.
2. Confirm it with the magic link or six-digit code we send you. Verifying your email credits your one-time signup grant of tokens.
3. Create an API key. It is shown once, at creation, so copy it then and store it securely. Never put a key in a URL.
4. Set it as an environment variable so your code and the SDKs can read it:
```bash
export CLOSECITY_KEY=ck_live_your_key_here
```
Every key begins `ck_live_`; there is no separate test mode. The free catalog routes below are the safe place to experiment without spending tokens.
You may hold up to 3 active keys per account. Rotate a key by creating a new one and revoking the old one.
## Free routes
[Section titled “Free routes”](#free-routes)
These routes need no key, so you can do all your lookups before spending a token:
* `GET /v1/health` — liveness
* `GET /v1/last-updated` — newest-data timestamp
* `GET /v1/meta/modes` — travel modes and their ids
* `GET /v1/meta/destination-types` — the amenity taxonomy
* `GET /v1/meta/vintage` — the version of each dataset component
* `GET /v1/places` — look up a city or town by name
* `GET /v1/isochrone/meta` — isochrone version, directions, and assumptions
Everything else is [metered](/guide/metering/).
# Citing Close
> How to cite the Close travel-time data in research, and how to pin the version so an analysis reproduces.
If you use Close in published work, please cite it, and record the data version so your analysis can be reproduced.
## Recommended citation
[Section titled “Recommended citation”](#recommended-citation)
> Close.City. *Close: travel times from every US census block to nearby points of interest.* Henry Spatial Analysis. (accessed YYYY-MM-DD).
BibTeX:
```bibtex
@misc{close_city,
title = {Close: travel times from every US census block to nearby
points of interest},
author = {{Close.City}},
howpublished = {\url{https://close.city}},
note = {Henry Spatial Analysis. Accessed YYYY-MM-DD}
}
```
## Pin the version
[Section titled “Pin the version”](#pin-the-version)
Close is rebuilt as its underlying sources change, so record the vintage you used. `GET /v1/meta/vintage` returns the live version of each component (`road_network`, `transit_network`, `pois`, `blocks`, `parks`), and `GET /v1/last-updated` gives the newest publication time. Both are free.
Report these versions in your methods, alongside the access date, so a reader can tell exactly which data produced your results. For how the travel times are computed and what they cover, see [The data](/guide/data/).
# Conditional requests
> Free revalidation with ETag and If-None-Match — poll for changes at no cost.
Metered `GET`s return an `ETag`. Send it back as `If-None-Match` to revalidate: if the data (and your query) are unchanged, you get `304 Not Modified` with no body and **no tokens charged**. This works even at a zero balance, so polling for changes is free.
```plaintext
GET /v1/blocks/{geoid}/summary → 200, ETag: "a1b2c3d4e5f6a7b8"
GET /v1/blocks/{geoid}/summary
If-None-Match: "a1b2c3d4e5f6a7b8" → 304 (0 tokens)
```
The ETag changes when either the underlying data vintage or your query changes. Treat it as an opaque validator — store it and echo it back verbatim.
Note
The isochrone routes carry their own long-lived, publicly cacheable ETags. One wrinkle: unlike the row-metered routes, an isochrone revalidation at a **zero balance** returns `429 tokens-exhausted` rather than a free `304`.
# Destination types
> The amenity taxonomy — the numeric type ids you filter on, and how parent types expand to leaves.
Every travel-time route filters by destination type, using the numeric `type` ids below. Look them up live and free at `GET /v1/meta/destination-types` (the SDKs return it as a data frame); this page is a readable copy so you can browse what is available. The live endpoint is always authoritative.
## Leaf and parent types
[Section titled “Leaf and parent types”](#leaf-and-parent-types)
A **leaf** type is one concrete amenity, such as grocery stores or high schools. A **parent** type is a grouping that expands to several leaves when you filter on it. Filtering by `parks` (a parent) returns rows for all four park-size leaves, which is four times the rows, and so four times the tokens, of a single leaf. When you want one amenity, filter by its leaf.
## Parent types
[Section titled “Parent types”](#parent-types)
| id | label | name | expands to |
| --- | -------------------- | -------------------------- | ------------------ |
| 1 | `public_schools` | Public schools | 5, 6, 7 |
| 43 | `libraries` | Libraries | 208, 209 |
| 60 | `all_transit` | All transit stops | 201, 202, 203, 204 |
| 61 | `frequent_transit` | Frequent transit stops | 201, 203 |
| 62 | `subway_lr` | Subway / light rail stops | 201, 202 |
| 63 | `parks` | Parks | 64, 65, 66, 67 |
| 200 | `nonfr_transit` | Non-frequent transit stops | 202, 204 |
| 205 | `other_transit` | Other transit stops | 203, 204 |
| 206 | `parks_gt_half_acre` | Parks (>0.5 acres) | 64, 65, 66 |
| 207 | `parks_gt_1_acre` | Parks (>1 acre) | 65, 66 |
## Leaf types
[Section titled “Leaf types”](#leaf-types)
| id | label | name |
| --- | ---------------------- | -------------------------------------- |
| 5 | `elementary_schools` | Elementary schools |
| 6 | `middle_schools` | Middle schools |
| 7 | `high_schools` | High schools |
| 27 | `restaurants` | Restaurants |
| 28 | `bars` | Bars |
| 29 | `convenience_stores` | Convenience stores |
| 30 | `grocery_stores` | Grocery stores |
| 31 | `cafes` | Cafes and coffee shops |
| 32 | `dentists` | Dentists |
| 33 | `gyms` | Gyms and exercise studios |
| 34 | `pharmacies` | Pharmacies |
| 35 | `preschools` | Preschools |
| 36 | `museums` | Museums |
| 37 | `thrift_stores` | Thrift stores |
| 38 | `community_centers` | Community centers |
| 39 | `farmers_markets` | Farmers markets |
| 40 | `bookstores` | Bookstores |
| 41 | `bike_shops` | Bike shops |
| 64 | `parks_half_to_1_acre` | Parks (0.5–1 acres) |
| 65 | `parks_1_to_10_acres` | Parks (1–10 acres) |
| 66 | `parks_gt_10_acres` | Parks (>10 acres) |
| 67 | `parks_lt_half_acre` | Parks (<0.5 acres) |
| 126 | `playgrounds` | Playgrounds |
| 159 | `bakeries` | Bakeries |
| 160 | `hardware_stores` | Hardware stores |
| 201 | `freq_subway_lr` | Frequent subway / light rail stops |
| 202 | `nonfr_subway_lr` | Non-frequent subway / light rail stops |
| 203 | `freq_other_transit` | Frequent other transit stops |
| 204 | `nonfr_other_transit` | Non-frequent other transit stops |
| 208 | `public_libraries` | Public libraries |
| 209 | `private_libraries` | University / private libraries |
Transit stops are split by frequency; parks are split by size. Pick the split that matches your question, or the parent when you want all of them.
*Generated from `GET /v1/meta/destination-types` on 2026-07-22. The live endpoint is authoritative.*
# Errors
> The RFC 9457 problem+json error model, its status codes, and stable slugs.
Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `problem+json`:
```json
{
"type": "https://api.close.city/problems/invalid-mode",
"title": "Unknown mode 'drive'.",
"status": 400,
"detail": "Valid modes: bike, transit, walk."
}
```
The stable, machine-readable key is the **last path segment of `type`** (the *slug*, e.g. `tokens-exhausted`) — match on it, not on the human `title`.
| Status | Slugs |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | `invalid-parameters`, `invalid-mode`, `invalid-type`, `invalid-cursor`, `invalid-geometry`, `invalid-bbox`, `missing-location`, `missing-area`, `polygon-too-complex` |
| 401 | `missing-key`, `invalid-key` |
| 403 | `account-disabled` |
| 404 | `block-not-found`, `poi-not-found`, `place-not-found`, `point-not-covered`, `no-isochrone-data`, `direction-not-available` |
| 429 | `rate-limited` (with `Retry-After`), `tokens-exhausted` |
| 503 | `debit-unavailable`, `store-unavailable` (not charged — retry; may carry `Retry-After`) |
Validation errors (`400 invalid-parameters`) also carry an `errors` array naming the offending fields. The [Python](/sdks/python/) and [R](/sdks/r/) SDKs map each slug to a typed exception / condition.
# Glossary
> The terms you will meet in the API — blocks, GEOIDs, modes, isochrones, catchments, cursors, and tokens.
**Census block** — the smallest area the US Census publishes, usually one city block. Close computes travel times from every block. See [The data](/guide/data/).
**GEOID** — the identifier for a census geography. A *block* GEOID is 15 digits; a *place* GEOID (a city or town, from `GET /v1/places`) is shorter. Block routes take a block GEOID; `place_blocks` takes a place GEOID.
**Census place** — an incorporated city or town. Look one up by name with `GET /v1/places` to get its GEOID and centre point.
**Mode** — how you travel: `walk`, `bike`, or `transit`. Block, point, and POI routes report the mode as a string label; the areal routes report it as a numeric `mode_id` (join `GET /v1/meta/modes` to label it).
**Destination type** — a category of amenity, identified by a numeric `type` id. A **leaf** type is one amenity (grocery stores); a **parent** type expands to several leaves. See [Destination types](/guide/destination-types/).
**Travel time** — minutes to the nearest amenity, capped at 30. A missing (block, type) row means the amenity is *not reachable* within the cap, not that it is zero.
**Isochrone** — the area reachable within a time budget, as **contours** (rings at each minute threshold). See [Isochrones](/guide/isochrones/).
**Catchment / walkshed** — the set of blocks that can reach a given amenity within a time budget (`poi_catchment`). “Walkshed” is a catchment for the walking mode.
**Cursor** — an opaque marker for the next page of a list result. Pass the `next_cursor` from one page to get the next; never construct one yourself. See [Pagination](/guide/pagination/).
**ETag / 304** — a version tag on a response. Send it back as `If-None-Match`; if nothing changed you get a free `304 Not Modified`. See [Conditional requests](/guide/conditional-requests/).
**Token** — the unit of metering: one per returned row, minimum one per request; isochrones cost 10 per contour. See [Metering](/guide/metering/).
**Vintage** — the version of a dataset component (road network, transit, POIs, blocks, parks). Read `GET /v1/meta/vintage` to see how current each one is.
# Isochrones
> Travel-time contours from a block — directions, contours, the two formats, and how they are priced.
An isochrone is the area reachable from (or to) a census block within a travel-time budget. `GET /v1/isochrone` returns it as GeoJSON contour polygons or as a list of reachable blocks. It is the cheapest way to ask “how far can I get”: one flat price regardless of how large the area is.
## Building a request
[Section titled “Building a request”](#building-a-request)
Identify the origin with `block=<15-digit GEOID>` or `lon=` + `lat=`. Then:
* `mode=walk|bike|transit` (default `walk`).
* `direction=to|from` (default `to`). `to` is the set of blocks that can *reach* the origin; `from` is where you can *get to* from the origin (the Mapbox-style travel shed). For a symmetric mode like walking the two are nearly the same; for transit they differ.
* `minutes=1..60` for a single threshold (default 30), or `contours=` with up to four ascending thresholds, for example `contours=10,20,30`.
* `format=geojson|blocks`.
Times use the same assumptions as the rest of the data: a weekday \~8am departure and a good-day (10th-percentile) transit trip. Every reply echoes them under `assumptions`, and `GET /v1/isochrone/meta` lists the version, directions, modes, and assumptions for free.
## The two contour properties
[Section titled “The two contour properties”](#the-two-contour-properties)
Each contour carries two values you will use:
* `contour` — the threshold in minutes (10, 20, 30 …). Features arrive largest contour first, so drawing them in order paints the shorter times on top.
* `reachable_blocks` — how many blocks fall inside that contour.
## Contour geometry
[Section titled “Contour geometry”](#contour-geometry)
Contours are drawn from block centres on an adaptive grid, so they describe block-level reachability, not a street-level boundary. Small interior holes (below about 1 square mile) are filled: a single large block such as a park or campus is one centre, so its footprint would otherwise read as an unreachable pocket that is really just a sampling artefact. Larger holes (lakes, airfields) are kept. Read a missing small hole as “not resolvable at block scale”, not as reachability.
## Two formats
[Section titled “Two formats”](#two-formats)
`format=geojson` returns a `FeatureCollection` of contour polygons, with the origin `block`, `direction`, `mode`, store `version`, and echoed `assumptions`. An empty contour has a `null` geometry.
`format=blocks` returns the reachable blocks with their travel minutes instead of polygons — handy when you want the block list, not the shape:
```json
{
"blocks": [ { "geoid": "440070036001010", "travel_min": 12 } ],
"reachable_blocks": 1,
"block": "440070036001010", "direction": "to", "mode": "walk"
}
```
## Pricing
[Section titled “Pricing”](#pricing)
Isochrones cost **10 tokens per contour level**, in either format: `minutes=30` costs 10, `contours=10,20,30` costs 30. The price is by level, not by rows, so a whole 30-minute walkshed as `format=blocks` is 10 tokens no matter how many blocks it contains.
Responses are strongly cacheable, and revalidating with `If-None-Match` is free on a match. One wrinkle, unlike the row-metered routes: an isochrone revalidation at a **zero balance** returns `429 tokens-exhausted` rather than a free `304`.
# Metering
> How tokens are charged — one per returned row, the isochrone exception, and the response headers.
One token is charged per returned data row, minimum one token per request — so a `200` with an empty `results` array still costs one token. The [isochrone](/reference/) is the exception: it charges 10 tokens per contour level (1–4 per call), regardless of format. A [`304 Not Modified`](/guide/conditional-requests/) costs nothing.
Every metered response carries:
| Header | Meaning |
| -------------------- | --------------------------------------------- |
| `X-Tokens-Charged` | Tokens charged for this request |
| `X-Tokens-Remaining` | Balance after this request |
| `X-Request-Id` | Correlation id (quote it in support requests) |
When the balance reaches zero, requests return `429` with problem type `tokens-exhausted`. You can see your token balance and buy more tokens at [account.close.city](https://account.close.city).
## What things cost
[Section titled “What things cost”](#what-things-cost)
Tokens are rows, so the cost of a call is the number of rows it returns. That gives you three levers: the modes, the destination types, and the blocks a query touches. A summary of a single block is a handful of rows; an areal query over a city is thousands. A few rules of thumb:
| Call | Rows returned | Typical cost |
| ------------------------------------- | ------------------------------- | --------------------- |
| A block or point summary | one per (category, mode) | tens of tokens |
| A POI search or a block’s POIs | one per matching POI | tens to a few hundred |
| A POI catchment (`poi_catchment`) | one per reachable block | hundreds |
| An areal block query (`blocks_query`) | one per (block, category, mode) | hundreds to thousands |
| Any isochrone | flat | 10 per contour level |
The isochrone is the cheap way to ask “how far can I get”: a whole 30-minute walkshed is 10 tokens as `format=blocks`, where the equivalent block query charges per block. See [Efficient token use](/guide/tips/) for the full playbook.
## Rate limits
[Section titled “Rate limits”](#rate-limits)
Each account may make up to **300 requests per minute**. Over that, requests return `429` with problem type `rate-limited` and a `Retry-After` header; the SDKs surface it as `RateLimitedError` (Python) or a `close_api_rate_limited` condition (R). The paginators default to 100 rows per page and accept up to 1000, so reading with `limit=1000` keeps you far under the limit. Very large bursts may also be shed at the gateway; back off and retry when you see a `429`.
## Getting tokens
[Section titled “Getting tokens”](#getting-tokens)
New accounts receive a **one-time signup grant of 5,000 tokens** on email verification, and you can buy token packs (**10,000 tokens for $10**) as one-time purchases at any time.
[Close+](https://account.close.city/plus), the $10/month membership for the [close.city](https://close.city) map, also grants **10,000 API tokens each paid month**, added to your balance while you are a member. Cancel anytime. Tokens you have already been granted stay on your account.
The SDKs surface `X-Tokens-Charged` / `X-Tokens-Remaining` on every reply, so you can watch the balance as you page.
# Pagination
> Opaque keyset cursors — how to page through list endpoints.
List endpoints use opaque keyset cursors. Pass `limit` (default 100, max 1000). When more rows remain, the response includes a non-null `next_cursor`; pass it back as `cursor` to fetch the next page:
```plaintext
GET /v1/blocks/{geoid}/pois?limit=500
→ { "results": [...], "next_cursor": "eyJ..." }
GET /v1/blocks/{geoid}/pois?limit=500&cursor=eyJ...
```
Cursors are signed — **do not construct or modify them**. A tampered cursor returns `400 invalid-cursor`.
Paginated endpoints: `/v1/pois`, `/v1/pois/{dest_id}/catchment`, `/v1/point/pois`, `/v1/blocks/query`, `/v1/places/{geoid}/blocks`, `/v1/blocks/{geoid}/pois`. The [Python](/sdks/python/) and [R](/sdks/r/) SDKs follow the cursor for you.
# Efficient token use
> How billing works and how to build queries that answer your question for the fewest tokens.
Tokens are rows: one per returned row, minimum one per request, and 10 per isochrone contour. So spending well is mostly about not asking for rows you will throw away. Here is the playbook.
## Free things
[Section titled “Free things”](#free-things)
* **The whole catalog is free and keyless.** `GET /v1/meta/modes`, `/v1/meta/destination-types`, `/v1/meta/vintage`, `/v1/places`, `/v1/isochrone/meta`, `/v1/last-updated`, and `/v1/health` cost nothing. Do all your lookups (type ids, city centres) before you spend a token.
* **Revalidation is free.** Cache a response with its `ETag` and send it back as `If-None-Match`; an unchanged response returns a free `304`, even at a zero balance (the one exception is isochrones). Watch `/v1/last-updated` to know when the data has actually changed. See [Conditional requests](/guide/conditional-requests/).
* **Failed requests are never charged**, and neither is population: passing `include_population=true` on an areal query adds a column, not tokens.
## The big levers
[Section titled “The big levers”](#the-big-levers)
The rows a query returns multiply out as **modes x types x blocks**. Each is a lever:
* **Always pass `mode`.** Omitting it returns all three modes, so three times the rows. Ask for the one you mean.
* **Request leaf types, not parents.** A parent type expands to its leaves: `parks` is four leaves (four times the rows), `frequent_transit` is two. Filter by the [leaf](/guide/destination-types/) you actually want.
* **Shrink the area.** Cost scales with the blocks you touch, and area grows with the square of the radius, so halving `radius_m` is roughly a quarter of the tokens.
* **Filter server-side.** `max_minutes` on the POI and catchment routes drops rows you would filter out anyway, so you pay for fewer.
## Isochrones are the cheap reach primitive
[Section titled “Isochrones are the cheap reach primitive”](#isochrones-are-the-cheap-reach-primitive)
An isochrone costs 10 tokens per contour regardless of how large the area is, and `format=blocks` returns every reachable block with its travel minutes. A whole 30-minute walkshed is 10 tokens, where the equivalent `poi_catchment` or `blocks_query` charges per block. Use catchments when you need per-POI stored times; use isochrones for “how far can I get” and for walkshed shapes. Four contours in one call cost the same as four separate calls but keep one consistent version.
## Mechanics
[Section titled “Mechanics”](#mechanics)
* **Page with `limit=1000`.** It does not change the token cost (tokens are rows), but it cuts the number of requests tenfold, which matters against the 300 requests-per-minute limit. The SDKs follow cursors for you.
* **Watch the balance while you learn.** Every metered reply carries `X-Tokens-Charged` and `X-Tokens-Remaining`; both SDKs surface them, on `df.attrs` (Python) or the frame’s attributes (R) in the tabular modes, and on the reply in `output="raw"`.
* **Reuse block geometry.** Block routes return GEOIDs; the SDKs join TIGER boundaries locally and cache them, so re-downloads cost time, not tokens. Pass a boundary frame you already have to skip the join, or use `output="tabular"` when you only want the numbers.
## What it costs
[Section titled “What it costs”](#what-it-costs)
Token packs are **10,000 tokens for $10**, so a token is a tenth of a cent and 1,000 rows is a dollar. A few real queries, measured:
| Query | Tokens | Cost |
| ---------------------------------------------------- | ----------- | -------------------- |
| Any catalog or place lookup | 0 | free |
| One point or block summary | \~5–20 rows | a fraction of a cent |
| A 30-minute walk isochrone | 10 | 1¢ |
| A three-contour isochrone (10/20/30 min) | 30 | 3¢ |
| Walk time to groceries for every block in Providence | 3,165 | \~$3.17 |
New accounts start with 5,000 free tokens — enough for that whole Providence table with room to spare, or roughly 500 single-contour isochrones, at no cost.
## What the tutorials cost
[Section titled “What the tutorials cost”](#what-the-tutorials-cost)
The SDK tutorials are designed to run comfortably within a new account’s signup grant. Each SDK tutorial page states its measured token cost at the top, so you can see what you are spending before you run it. The first-map tutorial is the cheapest place to start.
# Close API
> Travel times from every US census block to nearby points of interest — by walking, biking, and public transit. The data behind close.city, as a metered, read-only JSON API.
## Build with the Close travel-time database
[Section titled “Build with the Close travel-time database”](#build-with-the-close-travel-time-database)
[Quickstart](/start/quickstart/)[API reference](/reference/)[Python SDK](/sdks/python/)[R SDK](/sdks/r/)
## What’s in the data
[Section titled “What’s in the data”](#whats-in-the-data)
* **Travel times** from every US census block to nearby points of interest, by **walk**, **bike**, and **transit**, capped at 30 minutes.
* **Isochrones** — the area reachable from (or that can reach) a block within a time budget, as GeoJSON contours or a block list.
* **Points of interest** — search by location, with catchments and per-POI times.
* A **free catalog** of modes, destination types, and data vintages.
Keys are managed at [account.close.city](https://account.close.city).
## Tokens and Close+
[Section titled “Tokens and Close+”](#tokens-and-close)
The API is metered with prepaid tokens: one token per returned row, minimum one per request. See [Metering](/guide/metering/) for the rules and the balance headers. You can buy token packs at any time.
[Close+](https://account.close.city/plus) is the $10/month membership for the [close.city](https://close.city) map. It grants **10,000 API tokens each paid month** on top of everything it adds to the map, so if you use both, it is usually the cheapest way to keep a balance topped up. Cancel anytime.
# Python SDK
> closecity — a typed Python client that returns pandas and geopandas frames, with pagination, ETag/304, and token accounting built in.
`closecity` is a typed Python client over the Close API. It returns tabular results by default: a `geopandas.GeoDataFrame` where geometry applies (points, isochrone and block polygons) and a plain `pandas.DataFrame` otherwise.
```bash
pip install closecity
```
Note
The Python client is new. If `pip install closecity` cannot find it yet, install from the package source until the release lands on PyPI.
## Quickstart
[Section titled “Quickstart”](#quickstart)
```python
from closecity import Client
# Your API key (ck_live_) is created at https://account.close.city.
close = Client("ck_live_your_key_here")
# Catalog routes are free and come back as data frames.
types = close.destination_types()
# A radius search returns a GeoDataFrame of points, ready to map.
groceries = close.pois_search(lat = 41.82, lon = -71.41, radius_m = 1500)
groceries.plot()
```
## Choosing an output
[Section titled “Choosing an output”](#choosing-an-output)
Set `output` on the client, or per call:
* `output = "spatial"` (the default) returns a `GeoDataFrame` where geometry applies and a `DataFrame` otherwise.
* `output = "tabular"` returns a plain `DataFrame` for every route and never downloads block boundaries. This is the cheap path when you only want numbers.
* `output = "raw"` returns the underlying `Reply` / `Paginator`, with `.tokens_charged`, `.etag`, and the parsed body on `.data`.
```python
close.output = "raw"
summary = close.block_summary("440070008001068", mode = "walk")
print(summary.tokens_charged, "charged;", summary.tokens_remaining, "left")
```
Metering and envelope metadata ride on `df.attrs` in the frame modes.
## Full documentation
[Section titled “Full documentation”](#full-documentation)
The [Python SDK documentation](https://henryspatialanalysis.github.io/closecity-python/) has installation notes, a guided tour, worked tutorials, and the complete API reference. The [API reference here](/reference/) documents every endpoint and response shape.
# R SDK
> closecity for R — an R6 client that returns data frames and sf objects, built on httr2.
`closecity` is an R client over the Close API, built on `httr2`. It returns tabular results by default: an `sf` object where geometry applies (points, isochrone and block polygons) and a plain `data.frame` otherwise.
```r
# install.packages("remotes")
remotes::install_github("henryspatialanalysis/closecity-r")
```
Note
The R client is new. Until it is published to CRAN, install it from GitHub as above (or from the package source).
## Quickstart
[Section titled “Quickstart”](#quickstart)
```r
library(closecity)
# Your API key (ck_live_) is created at https://account.close.city.
close <- close_client("ck_live_your_key_here")
# Catalog routes are free and come back as data frames.
types <- close$destination_types()
# A radius search returns an sf of points, ready to map.
groceries <- close$pois_search(lat = 41.82, lon = -71.41, radius_m = 1500)
plot(sf::st_geometry(groceries))
```
## Choosing an output
[Section titled “Choosing an output”](#choosing-an-output)
Set `output` on the client, or per call:
* `output = "spatial"` (the default) returns an `sf` object where geometry applies and a `data.frame` otherwise.
* `output = "tabular"` returns a `data.frame` for every route and never downloads block boundaries. This is the cheap path when you only want numbers.
* `output = "raw"` returns the underlying `close_reply`, with `$tokens_charged`, `$etag`, and the parsed body on `$data`.
```r
close$output <- "raw"
summary <- close$block_summary("440070008001068", mode = "walk")
cat(summary$tokens_charged, "charged;", summary$tokens_remaining, "left\n")
```
Read every page of a paginated route with `close$records("pois_search", ...)`. Metering and envelope metadata are attached as attributes on the returned frame.
## Full documentation
[Section titled “Full documentation”](#full-documentation)
The [R SDK documentation](https://henryspatialanalysis.github.io/closecity-r/) has installation notes, a get-started vignette, worked tutorials, and the complete function reference. The [API reference here](/reference/) documents every endpoint and response shape.