Skip to content

For AI agents

This page is the compact reference for using the Close API from an LLM or agent. Humans: the Quickstart is friendlier.

  • Base URL: https://api.close.city
  • Auth: Authorization: Bearer <key>, where the key is a ck_live_ API key.
  • Machine contract: /openapi.json (OpenAPI 3.1, with a servers block and a bearer securityScheme). Also mirrored at 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.
  • 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.

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:

Terminal window
# 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 are RFC 9457 application/problem+json. Match on the slug (the last path segment of type), never the human title:

{ "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).

  • 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.
  • 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.

Close runs a remote Model Context Protocol 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:

Terminal window
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):

{
"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.