OpenSFHistory APIopensfhistory.org ↗
Access geolocated historical San Francisco photographs from OpenSFHistory. Retrieve coordinates, dates, photographer credits, and collection metadata via 3 endpoints.
What is the OpenSFHistory API?
The OpenSFHistory API provides access to the OpenSFHistory map collection through 3 endpoints, returning geolocated historical photographs of San Francisco with coordinates, dates, photographer names, and collection metadata. The get_mapped_images endpoint supports bounding-box filtering so you can retrieve images scoped to a specific neighborhood or block, while get_image_details returns the full metadata record for any individual photograph.
curl -X GET 'https://api.parse.bot/scraper/a27c5d27-17a2-455c-aaf2-cce506f24093/get_mapped_images' \ -H 'X-API-Key: $PARSE_API_KEY'
Typed, relational, agent-ready
A generated client with real types, enums, and the links between objects — the structure a flat JSON response can't carry. Autocompletes in your editor and reads cleanly to coding agents.
- Fully typed · autocompletes
- Objects link to objects
- Typed errors & pagination
Typed Python client. Set up the SDK in your uv project, then pull this API’s typed client:
uv add parse-sdk uv run parse init uv run parse add --marketplace opensfhistory-org-api
uv run parse add --marketplace pulls a pinned snapshot of this canonical API — it won’t change underneath you. To customize it, subscribe and swap to your own copy.
"""
OpenSFHistory Mapped Images API Client
Access historical San Francisco images with geographic locations and metadata.
Get your API key from: https://parse.bot/settings
"""
import os
import requests
from typing import Optional, List, Dict, Any
class ParseClient:
"""Client for the OpenSFHistory Mapped Images API."""
def __init__(self, api_key: Optional[str] = None):
"""
Initialize the Parse API client.
Args:
api_key: API key for authentication. If not provided, reads from PARSE_API_KEY env var.
"""
self.base_url = "https://api.parse.bot"
self.scraper_id = "a27c5d27-17a2-455c-aaf2-cce506f24093"
self.api_key = api_key or os.getenv("PARSE_API_KEY")
if not self.api_key:
raise ValueError("API key not provided and PARSE_API_KEY environment variable not set")
def _call(self, endpoint: str, method: str = "POST", **params) -> Dict[str, Any]:
"""
Make an API call to the Parse service.
Args:
endpoint: The endpoint name to call.
method: HTTP method (GET or POST).
**params: Query/body parameters.
Returns:
The JSON response from the API.
Raises:
requests.RequestException: If the API call fails.
"""
url = f"{self.base_url}/scraper/{self.scraper_id}/{endpoint}"
headers = {
"X-API-Key": self.api_key,
"Content-Type": "application/json"
}
if method.upper() == "GET":
response = requests.get(url, headers=headers, params=params)
elif method.upper() == "POST":
response = requests.post(url, headers=headers, json=params)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
return response.json()
def get_mapped_images(
self,
page: int = 1,
limit: int = 50,
min_lat: Optional[float] = None,
max_lat: Optional[float] = None,
min_lng: Optional[float] = None,
max_lng: Optional[float] = None
) -> Dict[str, Any]:
"""
Get paginated list of all mapped historical images with geographic coordinates.
Args:
page: Page number for pagination (default 1).
limit: Number of results per page, between 1-200 (default 50).
min_lat: Minimum latitude for bounding box filter.
max_lat: Maximum latitude for bounding box filter.
min_lng: Minimum longitude for bounding box filter.
max_lng: Maximum longitude for bounding box filter.
Returns:
Dictionary containing images list and pagination info.
"""
params = {
"page": page,
"limit": limit
}
if min_lat is not None:
params["min_lat"] = min_lat
if max_lat is not None:
params["max_lat"] = max_lat
if min_lng is not None:
params["min_lng"] = min_lng
if max_lng is not None:
params["max_lng"] = max_lng
return self._call("get_mapped_images", method="GET", **params)
def get_image_details(self, image_id: str) -> Dict[str, Any]:
"""
Get detailed metadata for a specific image.
Args:
image_id: The image identifier (e.g., 'wnp32.3610.jpg').
Returns:
Dictionary containing detailed image metadata.
"""
return self._call("get_image_details", method="GET", image_id=image_id)
def get_images_at_location(self, latitude: str, longitude: str) -> Dict[str, Any]:
"""
Get all images at a specific map coordinate.
Args:
latitude: Latitude of the map location as a string.
longitude: Longitude of the map location as a string.
Returns:
Dictionary containing images at the location.
"""
return self._call("get_images_at_location", method="GET", latitude=latitude, longitude=longitude)
def main():
"""Demonstrate practical usage of the OpenSFHistory API."""
# Initialize the client
client = ParseClient()
print("=" * 70)
print("OpenSFHistory Mapped Images API - Practical Usage Example")
print("=" * 70)
# Step 1: Get mapped images from San Francisco downtown area
print("\n📍 Step 1: Fetching historical images from San Francisco downtown...")
print(" (Using bounding box: 37.785°N to 37.795°N, -122.415°W to -122.405°W)")
response = client.get_mapped_images(
page=1,
limit=10,
min_lat=37.785,
max_lat=37.795,
min_lng=-122.415,
max_lng=-122.405
)
images = response.get("images", [])
total = response.get("total", 0)
print(f"✓ Found {total} total images in area, showing {len(images)} results:")
# Step 2: Display summary of found images
print("\n📷 Images found:")
for idx, image in enumerate(images[:5], 1): # Show first 5
print(f" {idx}. ID: {image['image_id']}")
print(f" Location: {image['latitude']:.6f}, {image['longitude']:.6f}")
print(f" URL: {image['image_url']}")
if len(images) == 0:
print(" No images found in this area. Trying a different location...")
# Try a broader area
response = client.get_mapped_images(page=1, limit=10)
images = response.get("images", [])
print(f" Found {len(images)} images in broader search")
# Step 3: Get details for the first image
if images:
first_image_id = images[0]["image_id"]
print(f"\n📖 Step 2: Fetching detailed metadata for image: {first_image_id}")
details = client.get_image_details(first_image_id)
print("\n✓ Image Details:")
print(f" Title: {details.get('title', 'N/A')}")
print(f" Date: {details.get('date', 'N/A')}")
print(f" Photographer: {details.get('photographer', 'N/A')}")
print(f" Collection: {details.get('collection', 'N/A')}")
print(f" Description: {details.get('description', 'N/A')[:100]}...")
print(f" Display URL: {details.get('display_url', 'N/A')}")
# Step 4: Get all images at the same location
print(f"\n🗺️ Step 3: Finding all images at location ({images[0]['latitude']}, {images[0]['longitude']})...")
location_images = client.get_images_at_location(
latitude=str(images[0]["latitude"]),
longitude=str(images[0]["longitude"])
)
location_count = location_images.get("count", 0)
location_results = location_images.get("images", [])
print(f"✓ Found {location_count} image(s) at this exact location:")
for img in location_results:
print(f" - {img['image_id']}: {img['title'][:60]}...")
# Step 5: Show pagination capability
print(f"\n📄 Step 4: Demonstrating pagination (getting page 2)...")
page_2 = client.get_mapped_images(page=2, limit=5)
print(f"✓ Page 2 results: {len(page_2.get('images', []))} images")
print(f" Total pages available: {page_2.get('total_pages', 'N/A')}")
print("\n" + "=" * 70)
print("✅ API demonstration complete!")
print("=" * 70)
if __name__ == "__main__":
main()Get paginated list of all mapped historical images with their geographic coordinates. Optionally filter by a geographic bounding box. Returns image IDs, coordinates, and URLs for each image.
| Param | Type | Description |
|---|---|---|
| page | integer | Page number for pagination. |
| limit | integer | Number of results per page. Must be between 1 and 200. |
| max_lat | number | Maximum latitude for bounding box filter (e.g. 37.80). |
| max_lng | number | Maximum longitude for bounding box filter (e.g. -122.40). |
| min_lat | number | Minimum latitude for bounding box filter (e.g. 37.75). |
| min_lng | number | Minimum longitude for bounding box filter (e.g. -122.45). |
{
"type": "object",
"fields": {
"page": "integer",
"limit": "integer",
"total": "integer",
"images": "array of image objects with image_id, latitude, longitude, image_url, display_url",
"total_pages": "integer"
},
"sample": {
"page": 1,
"limit": 5,
"total": 717,
"images": [
{
"image_id": "wnp25.6699.jpg",
"latitude": 37.750027,
"image_url": "https://www.opensfhistory.org/Image/wnp25.6699.jpg",
"longitude": -122.427414,
"display_url": "https://www.opensfhistory.org/Display/wnp25.6699.jpg"
}
],
"total_pages": 144
}
}About the OpenSFHistory API
What the API covers
All three endpoints draw from the OpenSFHistory mapped image collection — photographs that have been assigned geographic coordinates on the OpenSFHistory interactive map. Each image record carries a stable image_id (e.g. wnp32.3610.jpg), a latitude and longitude, an image_url for the full-resolution file, and a display_url for the preview version.
Browsing and filtering images
get_mapped_images returns a paginated list of all mapped images. You can page through results using the page and limit parameters (up to 200 per page) and narrow the result set to a geographic area using the four bounding-box parameters: min_lat, max_lat, min_lng, and max_lng. The response includes a total count and total_pages so you can drive your own pagination loop. Image IDs returned here feed directly into the other two endpoints.
Image metadata and location lookup
get_image_details accepts an image_id and returns the full metadata record: title, date, description, photographer, collection, and both image URL variants. get_images_at_location takes a precise latitude/longitude string and returns all images tagged to that exact coordinate, each with its title and image_id. Coordinates must match marker values from get_mapped_images — the matching is exact, not proximity-based.
Coverage notes
The dataset reflects OpenSFHistory's curated collection of San Francisco photographs. Only images that have been geographically mapped appear in results — unmapped items in the broader collection are not accessible through these endpoints. Date coverage spans the photographic history of the city, but the date field is a string and may contain ranges or approximate values as recorded in the original metadata.
The OpenSFHistory API is a managed, monitored endpoint for opensfhistory.org — not a raw scraper you maintain. Every endpoint is automatically health-checked on a schedule, and when opensfhistory.org changes and a check fails, the API is automatically queued for repair and re-verified. It is built to keep working as the site underneath it changes.
This isn't an official opensfhistory.org API — it's an independent, maintained REST wrapper over public data. Where the source has no official API (or only a limited one), Parse gives you a stable contract over a source that never promised one, and keeps it current. Need a new endpoint or field? You can revise it yourself in plain English and the agent rebuilds it against the live site in minutes — contributing the change back to the shared API is free.
Will this API break when the source site changes?+
Is this an official API from the source site?+
Can I fix or extend this API myself if I need a new endpoint or field?+
What happens if I call an endpoint that has an issue?+
- Build a neighborhood history map layer by querying
get_mapped_imageswith a bounding box for a specific SF district - Populate a timeline view of a city block by fetching
image_idvalues and resolvingdatefields fromget_image_details - Attribute photographer credits in a historical photo exhibit by reading the
photographerfield fromget_image_details - Cluster historical photos around a transit corridor using the
latitudeandlongitudefields fromget_mapped_images - Show all photos taken at a single corner or intersection using
get_images_at_locationwith exact coordinates - Cross-reference
collectionmetadata to identify which archive contributed a given photograph
| Tier | Price | Credits/month | Rate limit |
|---|---|---|---|
| Free | $0/mo | 100 | 5 req/min |
| Hobby | $30/mo | 1,000 | 20 req/min |
| Developer | $100/mo | 5,000 | 100 req/min |
One credit = one API call regardless of which marketplace API you call. Exceeding the rate limit returns a 429 response. Authenticate with the X-API-Key header.
Does OpenSFHistory have an official developer API?+
How does bounding-box filtering work in `get_mapped_images`?+
get_mapped_images accepts four optional parameters — min_lat, max_lat, min_lng, and max_lng — that define a geographic rectangle. Only images whose coordinates fall within that box are returned. Omitting all four parameters returns the full paginated dataset.Does `get_images_at_location` do proximity or radius-based matching?+
get_mapped_images results — for example, 37.327641 rather than a rounded value like 37.33. Passing an approximate coordinate will return zero results.Can I search for images by photographer name, date range, or keyword?+
get_mapped_images) or by exact coordinate (via get_images_at_location). Text-based fields like photographer, date, and description are returned in responses but cannot be used as query filters. You can fork this API on Parse and revise it to add a search endpoint that filters on those fields.