r311: An R interface to the open311 standard

Introduction

open311 is an international open-access standard for civic issue management and public service communication. The standard allows administrations to better manage citizen requests, citizens to more easily participate in administrative work, and researchers and data scientists to access data regarding public service communication. As an open standard, open311 is not a centralized API, but a framework implemented by various cities (e.g. San Francisco, CA, Chicago, IL, Cologne, DE, Turku, FI, Zurich, CH) and services (e.g. SeeClickFix, FixMyStreet).

It is way past the golden age of open311 APIs and much of development in civic issue tracking has shifted to less open-access and less standardized alternatives. Many former prime examples have abandoned or severely limited their open311 endpoints. Nonetheless, open311 still constitutes a valuable source for open government data. Many cities and services still maintain an open311 service.

r311 allows the seamless management and selection of endpoints and retrieval of service and request data. It supports (but does not depend on) many popular R frameworks such as the tidyverse, sf and xml2 for response formatting. r311 is designed to be slim, both in content and dependencies. It imports only two import-less packages used for HTTP response handling. The functionality is limited to two main features:

This vignette will briefly cover these two features.

Endpoints

Since open311 is an open and decentralized standard, there is not one but many endpoints that one can access. An endpoint is commonly implemented by a city administration, but can also be managed by a service provider such as FixMyStreet. Each endpoint can define multiple jurisdiction IDs, although, in practice, most endpoints only define a single jurisdiction.

It can thus be difficult to manage the multitude of endpoints and jurisdictions. Efforts have been made to list open311 servers, but most of them are incomplete or outdated. r311 offers an updated and modifiable endpoint list that defines a number of open311 implementations that are defined for use in the package. The list can be read using o311_endpoints.

o311_endpoints()
#> # A tibble: 67 × 9
#>    name            root  docs  jurisdiction key   pagination limit json  dialect
#>    <chr>           <chr> <chr> <chr>        <lgl> <lgl>      <int> <lgl> <chr>  
#>  1 Annaberg-Buchh… http… http… annberg-buc… FALSE TRUE          50 TRUE  Mark-a…
#>  2 Bloomington, IN http… <NA>  bloomington… FALSE TRUE        1000 TRUE  uReport
#>  3 Bonn, DE        http… http… bonn.de      FALSE TRUE         100 TRUE  Mark-a…
#>  4 Boston, MA      http… http… <NA>         FALSE TRUE          50 TRUE  Connec…
#>  5 Brookline, MA   http… http… brooklinema… FALSE TRUE          50 TRUE  Connec…
#>  6 Austin, TX      http… http… <NA>         FALSE TRUE          50 TRUE  Connec…
#>  7 Chicago, IL     http… http… cityofchica… FALSE TRUE          50 TRUE  Connec…
#>  8 Newport News, … http… http… cityofnewpo… FALSE TRUE          50 TRUE  Connec…
#>  9 San Diego, CA   http… http… sandiego.gov FALSE TRUE          50 TRUE  Connec…
#> 10 Köln / Cologne… http… <NA>  stadt-koeln… FALSE TRUE          50 TRUE  Mark-a…
#> # ℹ 57 more rows

The list does neither claim comprehensiveness nor up-to-dateness. It arguably provides some of the most important and easily accessible endpoints as of 2024. However, r311 also offers the ability to add new endpoints to o311_endpoints using o311_add_endpoint. You need to provide a name (for lookup) and a root URL (the URL used to send requests). The following code adds the open311 test server of Rostock, Germany.

o311_add_endpoint(
  name = "Rostock Test",
  root = "https://demo.klarschiff-hro.de/backoffice/citysdk/",
  jurisdiction = "rostock.de"
)

Retrieving the endpoints list again confirms that you successfully added a new row to the endpoints dataframe.

nrow(o311_endpoints())
#> [1] 68

You can now select the Rostock test API to the session using o311_api. This function matches an API using endpoint name and jurisdiction ID and attaches it to the active session. Query functions automatically detect the attached API.

o311_api("Rostock Test")

After attaching an API, o311_ok confirms that the selected endpoint is able to handle request queries.

o311_ok()
#> [1] TRUE

As the result is , you can safely start receiving real request data.

Making requests

After selecting an API and attaching it to the session, all functions can access it. You can now make requests.

Services

To get an overview of the available services in a jurisdiction, you can use o311_services, which returns a list of Rostock’s administrative services.

o311_services()
#> # A tibble: 161 × 8
#>    service_code service_name  description metadata type  keywords group group_id
#>    <chr>        <chr>         <lgl>       <lgl>    <chr> <chr>    <chr>    <int>
#>  1 86           Ampel behind… NA          FALSE    real… idea     Barr…      115
#>  2 87           Ampelschaltu… NA          FALSE    real… idea     Barr…      115
#>  3 88           Bordstein ab… NA          FALSE    real… idea     Barr…      115
#>  4 89           Zugang rolls… NA          FALSE    real… idea     Barr…      115
#>  5 90           Ampelschaltu… NA          FALSE    real… idea     Fahr…      120
#>  6 91           Beleuchtung … NA          FALSE    real… idea     Fahr…      120
#>  7 92           Beschilderun… NA          FALSE    real… idea     Fahr…      120
#>  8 93           Beschilderun… NA          FALSE    real… idea     Fahr…      120
#>  9 94           Bordstein ab… NA          FALSE    real… idea     Fahr…      120
#> 10 95           Fahrradständ… NA          FALSE    real… idea     Fahr…      120
#> # ℹ 151 more rows

Requests

To get data about civic issues in the city area, run o311_requests.

o311_requests()
#> Simple feature collection with 6 features and 15 fields
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: 12.07615 ymin: 54.06731 xmax: 12.23699 ymax: 54.2091
#> Geodetic CRS:  WGS 84
#> # A tibble: 6 × 16
#>   service_request_id status_notes status   service_code service_name description
#>                <int> <chr>        <chr>           <int> <chr>        <chr>      
#> 1                  3 ""           in_proc…           16 Grünschnitt… Dieser Wei…
#> 2                  2 ""           in_proc…           60 Sitzgelegen… Dieser Sit…
#> 3                  5 ""           closed             62 Ampel schad… Die Ampel …
#> 4                  6 ""           reviewed          110 Spielplatz … Hier sollt…
#> 5                  4 ""           closed            129 Papierkorb/… Dieser Abf…
#> 6                  1 ""           in_proc…          160 Altfahrzeug… Das Schrot…
#> # ℹ 10 more variables: agency_responsible <chr>, service_notice <lgl>,
#> #   requested_datetime <chr>, updated_datetime <chr>, expected_datetime <lgl>,
#> #   address <chr>, adress_id <lgl>, media_url <chr>, zipcode <lgl>,
#> #   geometry <POINT [°]>

Using the output of o311_services, you can further narrow down the output of requests. Open311 defines a set of standard parameters which are implemented by all endpoints. Using the service_code parameter with one of the previously returned service codes, only complaints about businesses illegally using shared spaces are returned.

o311_requests(service_code = "16")
#> Simple feature collection with 1 feature and 15 fields
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: 12.17547 ymin: 54.10655 xmax: 12.17547 ymax: 54.10655
#> Geodetic CRS:  WGS 84
#> # A tibble: 1 × 16
#>   service_request_id status_notes status   service_code service_name description
#>                <int> <chr>        <chr>           <int> <chr>        <chr>      
#> 1                  3 ""           in_proc…           16 Grünschnitt… Dieser Wei…
#> # ℹ 10 more variables: agency_responsible <chr>, service_notice <lgl>,
#> #   requested_datetime <chr>, updated_datetime <chr>, expected_datetime <lgl>,
#> #   address <chr>, adress_id <lgl>, media_url <chr>, zipcode <lgl>,
#> #   geometry <POINT [°]>

Similarly, using a service_request_id from the output, you can retrieve a single service request from the API.

o311_request("4")
#> Simple feature collection with 1 feature and 15 fields
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: 12.12266 ymin: 54.06731 xmax: 12.12266 ymax: 54.06731
#> Geodetic CRS:  WGS 84
#> # A tibble: 1 × 16
#>   service_request_id status_notes status service_code service_name   description
#>                <int> <chr>        <chr>         <int> <chr>          <chr>      
#> 1                  4 ""           closed          129 Papierkorb/Ab… Dieser Abf…
#> # ℹ 10 more variables: agency_responsible <chr>, service_notice <lgl>,
#> #   requested_datetime <chr>, updated_datetime <chr>, expected_datetime <lgl>,
#> #   address <chr>, adress_id <lgl>, media_url <chr>, zipcode <lgl>,
#> #   geometry <POINT [°]>

Bulk queries

Many endpoints define a page limit meaning that responses are divided into pages. A query without parameters returns the first page. Pagination can be controlled with the page argument. To control pagination, the o311_request_all function can come in handy. It automatically iterates through pages and heuristically decides when to stop. The following example retrieves data from the first two pages, resulting in a tibble with 100 service requests.

o311_api("Rostock, DE")
o311_request_all(max_pages = 2)
#> Simple feature collection with 4268 features and 15 fields
#> Geometry type: POINT
#> Dimension:     XY
#> Bounding box:  xmin: 12.01306 ymin: 54.05571 xmax: 12.20568 ymax: 54.20905
#> Geodetic CRS:  WGS 84
#> # A tibble: 4,268 × 16
#>    service_request_id status_notes  status service_code service_name description
#>                 <int> <chr>         <chr>         <int> <chr>        <chr>      
#>  1              24824 ""            in_pr…          113 Beschilderu… Kreuzung A…
#>  2              95322 "Danke für I… in_pr…            2 bauliche Ge… Gefährlich…
#>  3              70677  <NA>         recei…            2 bauliche Ge… redaktione…
#>  4              72069  <NA>         recei…           22 Sperrmüll    redaktione…
#>  5              72070  <NA>         recei…           15 Elektroschr… redaktione…
#>  6              70740  <NA>         revie…           22 Sperrmüll    Die 1.Meld…
#>  7              70076  <NA>         closed          160 Altfahrzeug… <NA>       
#>  8              75356  <NA>         recei…           17 Hausmüll     redaktione…
#>  9              43518 "Der Erdhauf… in_pr…           13 Bauabfall    Illegale E…
#> 10              44333  <NA>         recei…            2 bauliche Ge… redaktione…
#> # ℹ 4,258 more rows
#> # ℹ 10 more variables: agency_responsible <chr>, service_notice <lgl>,
#> #   requested_datetime <chr>, updated_datetime <chr>, expected_datetime <chr>,
#> #   address <chr>, adress_id <lgl>, media_url <chr>, zipcode <lgl>,
#> #   geometry <POINT [°]>

Non-standard parameters

r311 implicitly supports API extensions introducing custom paths and parameters. One such API is Klarschiff Rostock which is based on CitySDK. Klarschiff CitySDK defines a number of non-default paths and parameters which extend the filtering abilities of open311 requests. Available parameters can usually be found in the respective documentation (e.g. on GitHub for Klarschiff CitySDK). The following query returns the last 50 requests tagged with the “idea” keyword.

tickets <- o311_requests(keyword = "idea", max_requests = 50)

Some custom parameters can also alter the shape of the output. In the following example, we query just the count of total requests using the just_count parameter. The result is a 1×1 tibble containing a count value.

o311_requests(just_count = TRUE)
#> # A tibble: 1 × 1
#>   value
#>   <int>
#> 1  4268

The CitySDK extensions also offers additional URL paths which can be queried using the generic o311_query function.

poly <- o311_query("areas")
plot(poly$grenze)
plot(tickets$geometry, add = TRUE, pch = 16)

Cleanup

Endpoint data is stored persistently between sessions so that you can create your own database of open311 endpoints. This database is stored in the system’s user directory as returned by tools::R_user_dir("r311"). To reset the database, run

o311_reset_endpoints()

This will default back to the endpoints defined by r311 and remove all endpoints manually added by o311_add_endpoints.