City 311 Requests Scraper: Potholes, Parking & More (Open311) avatar

City 311 Requests Scraper: Potholes, Parking & More (Open311)

Pricing

from $0.35 / 1,000 records

Go to Apify Store
City 311 Requests Scraper: Potholes, Parking & More (Open311)

City 311 Requests Scraper: Potholes, Parking & More (Open311)

Download public 311 requests (potholes, illegal parking, streetlights) from Open311 cities such as San Francisco. One row per request: ID, status, service type, description, address, lat/lon, media link, timestamps. Filter by open/closed, service code or dates, or list a city's service types.

Pricing

from $0.35 / 1,000 records

Rating

0.0

(0)

Developer

Datamule

Datamule

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

a month ago

Last modified

Categories

Share

What does Open311 Service Request Extractor do?

Open311 Service Request Extractor turns municipal 311 feeds into structured, agent-ready records. Point it at any compatible Open311 GeoReport v2 jurisdiction to collect public service requests, the jurisdiction's service catalog, or its discovery document. Request rows include the service-request ID, status, service type, description, address, coordinates, media, timestamps, source identity, and the complete source object in _raw.

Run it on Apify to access results through the API, schedule recurring collection, connect datasets to other tools, and monitor runs without maintaining scraper infrastructure.

Why use Open311 Service Request Extractor?

  • Municipal operations agents: monitor new or unresolved pothole, sanitation, parking, graffiti, and streetlight reports.
  • Civic research: compare issue types, response states, and reporting patterns across compatible jurisdictions.
  • Alerts and workflows: feed new requests into a queue, dashboard, geospatial pipeline, or incident-triage agent.
  • Service discovery: list valid service_code values before building a filtered request workflow.
  • Lossless downstream processing: use normalized columns for common analysis while retaining each complete source object in _raw.

How to use Open311 Service Request Extractor

  1. Open the Actor's Input tab.
  2. Paste an Open311 jurisdiction base URL or full JSON endpoint.
  3. Choose requests, services, or discovery mode.
  4. Keep the default 25-row cap for a small first run, or set another positive cap.
  5. Start the run and open the Dataset tab to inspect or export the rows.

Try this exact input:

{"jurisdiction":"https://mobile311.sfgov.org","maxRecords":25,"mode":"requests"}

Input

FieldTypeDescription
jurisdictionstringOne Open311 base URL or full requests.json, services.json, or discovery.json endpoint. A copied URL with ?status=open&service_code=… works too: those filters are used when the matching field is empty, and a value typed in the form wins. Other URL parameters such as api_key are not sent, and their values are shown as REDACTED in _source and the log.
jurisdictionsarrayOptional multi-city list. The global row cap applies across all sources.
modestringrequests (default), services, or discovery.
maxRecordsintegerGlobal output cap. Omitted, invalid, or non-positive values resolve to 25; every explicit positive cap is preserved.
serviceCodestringRequests-mode filter passed as service_code. Run services mode to discover valid codes.
statusstringRequests-mode filter: all, open, or closed.
startDate, endDatestringOptional RFC 3339 date-time window for requests.
serviceRequestIdstringOne ID or a comma-separated list of request IDs.
jurisdictionIdstringOptional identifier for multi-tenant Open311 servers.
tokensecret stringOptional bearer credential for a jurisdiction that requires authentication. It is never logged.
extraHeadersobjectOptional request headers for a gateway-fronted source. Header values are never logged.

Output

Fresh local acceptance with the exact try input above returned 25 genuine service requests and stopped at the cap. The following is an abridged row from that San Francisco Open311 run; actual dataset rows include every nullable schema column plus the complete source object in _raw.

{
"_type": "service_request",
"_source": "https://mobile311.sfgov.org",
"_endpoint": "https://mobile311.sfgov.org/open311/v2/requests.json",
"_mode": "requests",
"_software": "open311",
"serviceRequestId": "101004471550",
"status": "open",
"serviceName": "Blocked driveway and illegal parking",
"serviceCode": "MTA:Parking Enforcement:General Enforcement",
"description": "Jeep blocking pedestrian path",
"requestedDatetime": "2026-07-21T02:54:51Z",
"updatedDatetime": "2026-07-21T02:55:29Z",
"address": "164 29 Th St",
"lat": 37.74398683,
"long": -122.42360781
}

You can download the dataset in formats such as JSON, HTML, CSV, or Excel, or read it through the Apify API.

Data table and field dictionary

FieldMeaning
_typeservice_request, service, or discovery.
_source, _jurisdictionJurisdiction value supplied to the Actor (non-filter URL parameter values replaced by REDACTED) and optional multi-tenant jurisdiction ID.
_endpointResolved Open311 JSON endpoint.
_mode, _softwareNormalized mode and open311 software identity.
serviceRequestIdJurisdiction-issued request ID.
status, statusNotesCurrent request state and optional status detail.
serviceName, serviceCodeHuman-readable service and machine filter code.
descriptionPublic issue description supplied by the source.
requestedDatetime, updatedDatetime, expectedDatetimeNormalized source timestamps when present.
address, addressId, zipcode, lat, longSource-provided location fields.
mediaUrlOptional public media URL.
requestTokenSource-provided Open311 request-tracking token, when present.
agencyResponsible, serviceNoticeOptional routing and service-level information.
metadata, type, group, keywordsService-catalog fields used in services mode.
changeset, contact, keyService, specification, endpointUrls, endpointsDiscovery fields used in discovery mode.
_rawComplete, lossless JSON object returned by the source.

How much does it cost to extract Open311 data?

The active FREE-tier pay-per-event price is $0.0005 per emitted record. The default first run emits at most 25 rows, so its FREE-tier PPE event-charge ceiling is:

25 × $0.0005 = $0.0125

This is an event-charge ceiling, not the total run cost. Apify infrastructure usage, such as compute, storage, or a user-selected proxy, is separate. A smaller positive cap lowers the ceiling; a larger positive cap raises it proportionally.

Tips, coverage, freshness, and limits

  • Start with services mode to discover jurisdiction-specific service codes, then use serviceCode in requests mode.
  • Use startDate, endDate, and status to reduce volume and focus an alerting or monitoring job.
  • maxRecords is global across a multi-jurisdiction run, not a per-city cap.
  • Open311 servers control data freshness and retention. Results represent what the source returned at run time; many request feeds default to a recent window.
  • Pagination behavior varies because GeoReport v2 does not standardize it. The Actor de-duplicates request IDs and stops when a server repeats a page or the global cap is reached.
  • Missing optional fields remain null; they are not fabricated.

Export open requests to CSV from your own code (Python, standard library only)

This script starts one small run for open San Francisco requests, waits for it, and saves a narrow CSV (request ID, status, service, timestamps, address and coordinates). Set APIFY_TOKEN in your environment first; the script sends it as a Bearer header and never prints it. Change jurisdiction, status or maxRecords in RUN_INPUT for another city or filter.

import csv, json, os, sys, time, urllib.error, urllib.request
TOKEN = os.environ["APIFY_TOKEN"] # read from the environment, never printed
API = "https://api.apify.com/v2"
ACTOR = "datamule~open311-service-request-extractor"
RUN_INPUT = {
"jurisdiction": "https://mobile311.sfgov.org", # public San Francisco 311 endpoint
"mode": "requests",
"status": "open",
"maxRecords": 10,
}
MAX_CHARGE_USD = 0.01 # hard spend cap for this run
WAIT_SECONDS = 300
COLUMNS = ["serviceRequestId", "status", "serviceCode", "serviceName",
"requestedDatetime", "updatedDatetime", "address", "zipcode", "lat", "long"]
def call(method, path, body=None):
req = urllib.request.Request(
API + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as resp:
return json.load(resp)
try:
run = call("POST", f"/acts/{ACTOR}/runs?maxTotalChargeUsd={MAX_CHARGE_USD}", RUN_INPUT)["data"]
deadline = time.time() + WAIT_SECONDS
while run["status"] in ("READY", "RUNNING"):
if time.time() > deadline:
sys.exit(f"Run {run['id']} still {run['status']} after {WAIT_SECONDS}s; no CSV written.")
time.sleep(10)
run = call("GET", f"/actor-runs/{run['id']}")["data"]
if run["status"] != "SUCCEEDED":
sys.exit(f"Run {run['id']} ended {run['status']}; no CSV written.")
rows = call("GET", f"/datasets/{run['defaultDatasetId']}/items?clean=1&format=json")
except (urllib.error.URLError, OSError, KeyError, ValueError) as err:
sys.exit(f"API call failed ({type(err).__name__}); no CSV written.")
if not isinstance(rows, list):
sys.exit("Unexpected dataset response; no CSV written.")
requests = [r for r in rows if r.get("_type") == "service_request"]
if rows and not requests:
sys.exit(f"Run {run['id']} returned {len(rows)} row(s) but none were service requests; no CSV written.")
out = f"open311_open_requests_{run['id']}.csv"
with open(out, "w", newline="", encoding="utf-8") as fh:
writer = csv.DictWriter(fh, fieldnames=COLUMNS, extrasaction="ignore")
writer.writeheader()
writer.writerows(requests)
if requests:
print(f"Run {run['id']} SUCCEEDED: wrote {len(requests)} open request(s) to {out}")
else:
print(f"Run {run['id']} SUCCEEDED with 0 rows: the endpoint returned no open requests "
f"this time. {out} has only the header.")
  • Caps: maxRecords: 10 limits rows, and maxTotalChargeUsd=0.01 limits what this run can charge. Raise both together for bigger exports.
  • Not a full history: the CSV holds up to 10 open requests the San Francisco server returned at run time, in the server's order. It is not every open request, and many Open311 servers only return a recent window.
  • Freshness: data is as fresh as the city server at run time; re-run it or schedule the Actor for updates. This script does not detect new or changed requests between runs.
  • Zero rows is still a success: the script says so and writes a header-only CSV. A failed, aborted or timed-out run, an API error, or an unexpected response (including rows that are not service requests) exits non-zero and writes no CSV.
  • Waiting: the script stops waiting after 5 minutes. The run may keep going on Apify, still within the charge cap; check it in Console.
  • Empty cells mean the city did not publish that field (for example zipcode in San Francisco).

FAQ, public-data scope, troubleshooting, and support

Which sources are supported? Compatible Open311 GeoReport v2 JSON endpoints. A normal website root is accepted only when the standard /open311/v2/ paths exist. If a root is invalid, use the jurisdiction's full JSON endpoint.

Why was a source skipped after returning HTML? The URL is probably a website page, anti-bot interstitial, or non-Open311 endpoint. Confirm that requests.json, services.json, or discovery.json returns JSON.

What if the source requires authentication? Supply your own token or extraHeaders. The Actor does not bypass access controls. Invalid credentials, forbidden replies, and unavailable sources are skipped with a warning; a run where every source is invalid fails clearly.

Why did a valid run return zero rows? The selected date range, status, service code, or request ID may have no matches. Widen or remove filters, try requests mode, or check the source directly. A valid zero-row response is kept distinct from an invalid source.

Is the data private? This Actor is designed for public civic-data endpoints. Records can still contain addresses, free-text descriptions, or media supplied to a municipality. Review the source's terms, applicable privacy rules, and your downstream retention policy before redistributing data.

Open311 implementations differ and can change without notice. Use the Actor's Issues tab to report a source-specific problem or request a custom integration.