Discover/OpenSFHistory API
live

OpenSFHistory APIopensfhistory.org

Access geolocated historical San Francisco photographs from OpenSFHistory. Retrieve coordinates, dates, photographer credits, and collection metadata via 3 endpoints.

This API takes change requests — .
Endpoints
3
Updated
3d ago

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.

This call costs5 credits / call— charged only on success
Try it
Page number for pagination.
Number of results per page. Must be between 1 and 200.
Maximum latitude for bounding box filter (e.g. 37.80).
Maximum longitude for bounding box filter (e.g. -122.40).
Minimum latitude for bounding box filter (e.g. 37.75).
Minimum longitude for bounding box filter (e.g. -122.45).
api.parse.bot/scraper/a27c5d27-17a2-455c-aaf2-cce506f24093/<endpoint>
Ready to send
Fill in the parameters and hit sign in to send to see live response data here.
Call it over HTTPgrab a free API key at signup
curl -X GET 'https://api.parse.bot/scraper/a27c5d27-17a2-455c-aaf2-cce506f24093/get_mapped_images' \
  -H 'X-API-Key: $PARSE_API_KEY'
Python SDK · recommended

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()
All endpoints · 3 totalmissing one? ·

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.

Input
ParamTypeDescription
pageintegerPage number for pagination.
limitintegerNumber of results per page. Must be between 1 and 200.
max_latnumberMaximum latitude for bounding box filter (e.g. 37.80).
max_lngnumberMaximum longitude for bounding box filter (e.g. -122.40).
min_latnumberMinimum latitude for bounding box filter (e.g. 37.75).
min_lngnumberMinimum longitude for bounding box filter (e.g. -122.45).
Response
{
  "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.

Reliability & maintenance

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?+
It's built not to. Every endpoint is health-checked on a schedule with automated test probes. When the source site changes and a check fails, the API is automatically queued for repair and re-verified — that's the self-healing layer. Each API page shows when its endpoints were last verified. And because marketplace APIs are shared, any fix reaches everyone using it.
Is this an official API from the source site?+
No — Parse APIs are independent, managed REST wrappers over publicly available data. That is the point: where a site has no official API (or only a limited one), Parse gives you a maintained, monitored endpoint for that data and keeps it working as the site changes — so you get a stable contract over a source that never promised one.
Can I fix or extend this API myself if I need a new endpoint or field?+
Yes — and you don't have to wait on us. This API was generated by the Parse agent, which stays attached. Describe the change in plain English ("add an endpoint that returns reviews", "fix the price field") in the revise box on the API page or via the revise_api MCP tool, and the agent rebuilds it against the live site in minutes. Contributing the change back to the public API is free.
What happens if I call an endpoint that has an issue?+
Errors are machine-readable: a bad call returns a clean status with the list of available endpoints and a repair hint, so an agent (or you) can recover or trigger a fix instead of failing silently. Confirmed failures feed the automatic repair queue.
Common use cases
  • Build a neighborhood history map layer by querying get_mapped_images with a bounding box for a specific SF district
  • Populate a timeline view of a city block by fetching image_id values and resolving date fields from get_image_details
  • Attribute photographer credits in a historical photo exhibit by reading the photographer field from get_image_details
  • Cluster historical photos around a transit corridor using the latitude and longitude fields from get_mapped_images
  • Show all photos taken at a single corner or intersection using get_images_at_location with exact coordinates
  • Cross-reference collection metadata to identify which archive contributed a given photograph
Pricing & limitsSee full pricing →
TierPriceCredits/monthRate limit
Free$0/mo1005 req/min
Hobby$30/mo1,00020 req/min
Developer$100/mo5,000100 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.

Frequently asked questions
Does OpenSFHistory have an official developer API?+
OpenSFHistory does not publish an official developer API. The site's map interface is designed for public browsing rather than programmatic access.
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?+
No. It performs exact string matching against the coordinate values stored in the marker dataset. Coordinates must match precisely as they appear in 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?+
Not currently. The API supports filtering only by geographic bounding box (via 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.
Are images outside San Francisco included in the collection?+
The OpenSFHistory collection focuses on San Francisco. The coordinate data in the dataset is clustered within SF boundaries. Images mapped to locations outside the city proper are not a documented part of the collection, so results from adjacent cities or regions should not be expected.
Page content last updated . Spec covers 3 endpoints from opensfhistory.org.
Related APIs in Maps GeoSee all →
geonames.org API
Search for places worldwide and get their exact coordinates, timezone information, and elevation data, or reverse lookup locations by coordinates to discover nearby areas. Access postal codes, country details, and geographic names across the globe to build location-aware applications and services.
portofrotterdam.com API
Track live vessel movements and monitor port performance metrics including container throughput and anchorage statistics for the Port of Rotterdam. Access nautical notices and search detailed port information to stay updated on shipping operations and port conditions.
expatistan.com API
Compare cost of living across cities and countries worldwide, view rankings, and analyze expense data to make informed decisions about relocating or understanding living costs globally. Search for specific cities and access the latest pricing information on housing, food, transportation, and other essential expenses.
citymapper.com API
Get real-time transit information including live stop arrivals, service status, and line details across major cities worldwide. Search for nearby transit options and stay informed with service alerts to plan your commute efficiently.
marinetraffic.com API
Track maritime vessels in real-time by searching for ships by name, MMSI, or IMO number, viewing their current positions and navigational status, and accessing detailed specifications and photos. Filter vessels by type to get the maritime intelligence you need for shipping, logistics, or maritime awareness.
mbta.com API
Track real-time subway, bus, and commuter rail departures across Greater Boston, check schedules and service alerts, and find detailed information about routes and stops. Plan your commute with up-to-the-minute MBTA transit data and never miss your connection.
plugshare.com API
Search for EV charging stations worldwide by location and radius. Retrieve real-time availability, connector types, user reviews, and amenity details for any station. Filter by residential or commercial property type to find chargers at apartment complexes, parking facilities, and more.
bart.gov API
Track live BART train departures and arrival estimates across all Bay Area stations in real-time. Find your nearest station and see exactly when the next train is arriving on every platform.