OpenBookCase Developer API

Build apps on the OpenBookCase data. Reading public data is open — no key needed. Writing (adding/editing entries, photos, wishlists, …) needs an OAuth 2.0 access token that acts on behalf of one of your users. All requests are JSON in / JSON out and live under /api/v1.

Base URL in production: https://openbookcase.de

How it works

  • Open reads: the map area listing, single entries, their photos and wishlists are public — call them with no token.
  • Authenticated writes: every change is made as one of your users. They authorize your app once (OAuth), and your token then creates/edits data under their name.
  • No account endpoints: there is deliberately no API for e‑mail, password, notifications or account deletion.
  • Open data: the dataset is OpenStreetMap-derived and licensed under the ODbL — see Data terms.

Getting access

Open your Profile → API access and submit an application describing your app and how the data will be used. An admin reviews it (there's a built-in back-and-forth if they have questions). On approval you receive your credentials:

  • Public client (mobile / single-page apps): a client_id, no secret. Must use PKCE.
  • Confidential client (apps with a secure backend): a client_id and a client_secret shown once — store it safely.

You also register one or more redirect URIs and pick the scopes you need.

Authentication (OAuth 2.0 Authorization Code)

Two endpoints: /oauth/authorize (the user approves your app) and /oauth/token (you exchange the resulting code for tokens). Access tokens last 1 hour; refresh tokens last 1 year (rotated on use). Tokens stay valid until revoked.

1 · Public clients (mobile/SPA) — PKCE

First create a PKCE verifier + challenge:

# code_verifier: 43–128 random chars; code_challenge = base64url(sha256(verifier))
CODE_VERIFIER=$(openssl rand -base64 60 | tr -d '\n=+/' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
  | openssl dgst -binary -sha256 | openssl base64 | tr '+/' '-_' | tr -d '=')

Send the user to the authorize URL (open it in a browser / in-app browser):

https://openbookcase.de/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.example/callback
  &scope=bookcases.write%20images.write
  &state=RANDOM_STRING
  &code_challenge=$CODE_CHALLENGE
  &code_challenge_method=S256

After the user approves, they're redirected to https://yourapp.example/callback?code=AUTH_CODE&state=RANDOM_STRING.

Exchange the code for tokens (public clients send the verifier, no secret):

curl -X POST https://openbookcase.de/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d redirect_uri=https://yourapp.example/callback \
  -d code_verifier=$CODE_VERIFIER \
  -d code=AUTH_CODE

Response:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1Qi...",
  "refresh_token": "def502..."
}

2 · Confidential clients (with a backend)

Same flow, but you authenticate with your secret instead of a PKCE verifier:

curl -X POST https://openbookcase.de/oauth/token \
  -d grant_type=authorization_code \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d redirect_uri=https://yourapp.example/callback \
  -d code=AUTH_CODE

3 · Refreshing a token

curl -X POST https://openbookcase.de/oauth/token \
  -d grant_type=refresh_token \
  -d client_id=YOUR_CLIENT_ID \
  -d refresh_token=YOUR_REFRESH_TOKEN
  # confidential clients also add: -d client_secret=YOUR_CLIENT_SECRET

Scopes

Request only what you need (space-separated in the scope parameter). Reads need no scope.

ScopeGrants
bookcases.writeAdd & edit entries, move their position
bookcases.deleteDelete an entry (a reason is required; a snapshot is archived)
images.writeUpload photos to an entry
wishlist.writeAdd wishlist items to an entry
watchlist.writeWatch / unwatch an entry for the user
ratings.writeRate an entry (1–5) for the user, and remove their rating
home.writeSet the user's home map location

Endpoint reference

MethodPathAuthPurpose
GET/api/v1/bookcasesopenList by bounding box (latMin,latMax,lonMin,lonMax, limit≤1000, offset)
GET/api/v1/bookcases/{id}openSingle entry with full detail
GET/api/v1/bookcases/{id}/imagesopenList an entry's photos
GET/api/v1/bookcases/{id}/wishlistopenList an entry's wishlist items
GET/api/v1/bookcases/{id}/caretakersopenList an entry's caretakers
GET/api/v1/bookcases/{id}/opening-timesopenList an entry's opening times
GET/api/v1/bookcases/{id}/ratingopenRating summary (count + average)
POST/api/v1/bookcasesbookcases.writeCreate an entry
PATCH/api/v1/bookcases/{id}bookcases.writeUpdate fields (partial)
POST/api/v1/bookcases/{id}/positionbookcases.writeMove (latitude/longitude)
DELETE/api/v1/bookcases/{id}bookcases.deleteDelete (JSON reason required)
POST/api/v1/bookcases/{id}/imagesimages.writeUpload a photo (multipart)
POST/api/v1/bookcases/{id}/wishlistwishlist.writeAdd a wishlist item
POST/api/v1/bookcases/{id}/watchwatchlist.writeWatch an entry
DELETE/api/v1/bookcases/{id}/watchwatchlist.writeUnwatch an entry
POST/api/v1/profile/homehome.writeSet the user's home location
POST/api/v1/bookcases/{id}/caretakersbookcases.writeAdd a caretaker to an entry
PATCH/api/v1/bookcases/{id}/caretakers/{caretakerId}bookcases.writeUpdate a caretaker (partial)
DELETE/api/v1/bookcases/{id}/caretakers/{caretakerId}bookcases.writeRemove a caretaker
POST/api/v1/bookcases/{id}/opening-timesbookcases.writeAdd an opening time
PATCH/api/v1/bookcases/{id}/opening-times/{openingTimeId}bookcases.writeUpdate an opening time (partial)
DELETE/api/v1/bookcases/{id}/opening-times/{openingTimeId}bookcases.writeRemove an opening time
PUT/api/v1/bookcases/{id}/ratingratings.writeAdd or update the user's rating (1–5)
DELETE/api/v1/bookcases/{id}/ratingratings.writeRemove the user's rating

Bearer token goes in the Authorization header. A write without a token returns 401; with the wrong scope, 403.

Using the API

Read the map area (open)

curl "https://openbookcase.de/api/v1/bookcases?latMin=52.4&latMax=52.6&lonMin=13.3&lonMax=13.5&limit=200"

Read one entry (open)

curl https://openbookcase.de/api/v1/bookcases/01JZ8M4P3Q0000000000000000

Create an entry

curl -X POST https://openbookcase.de/api/v1/bookcases \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Community shelf at the park",
        "entryType": "bookcase",
        "latitude": 52.5200,
        "longitude": 13.4050
      }'
# → 201 Created, returns the new entry (incl. its id). Titles may not contain URLs.

Update fields (partial)

curl -X PATCH https://openbookcase.de/api/v1/bookcases/01JZ8M4P3Q... \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"comment": "Refilled weekly", "isMobile": false, "accessibilityLevel": 3}'

Move an entry

curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ8M4P3Q.../position \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"latitude": 52.5198, "longitude": 13.4042}'

Upload a photo (multipart)

curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ8M4P3Q.../images \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -F imageFile=@shelf.jpg \
  -F author="Jane Doe" \
  -F altText="Front view of the shelf"
# imageFile + author are required; altText is optional. Max 5 photos per entry.

Delete an entry (reason required)

curl -X DELETE https://openbookcase.de/api/v1/bookcases/01JZ8M4P3Q... \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Bookcase was removed by the property owner"}'

Watch · wishlist · home

# Watch an entry (get notified of changes)
curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ.../watch \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# Add a wishlist item (title required; author/isbn/misc optional)
curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ.../wishlist \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"title": "The Left Hand of Darkness", "author": "Ursula K. Le Guin"}'

# Set the user's home map location
curl -X POST https://openbookcase.de/api/v1/profile/home \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"latitude": 52.52, "longitude": 13.405, "zoom": 14}'

Caretakers, opening times & ratings

Caretakers and opening times are part of the entry, so they use bookcases.write. A rating belongs to the user, so it uses ratings.write.

# Add a caretaker (a name and/or contact is required; address is optional)
curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ.../caretakers \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"name": "Local library", "contact": "hello@example.org",
       "address": {"street": "Main St", "houseNumber": "5", "city": "Berlin"}}'
# → 201 Created, returns the caretaker (incl. its id). PATCH .../caretakers/{id} edits it,
#   DELETE .../caretakers/{id} detaches it (and removes it if no entry uses it any more).

# Add an opening time (free-text text, OR the 24/7 flag — one is required)
curl -X POST https://openbookcase.de/api/v1/bookcases/01JZ.../opening-times \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"openTime": "Mo-Fr 08:00-20:00"}'        # or: {"twentyFourSeven": true}
# PATCH/DELETE .../opening-times/{id} edit or remove it.

# Rate the entry for the user (1–5; creates or updates their single rating)
curl -X PUT https://openbookcase.de/api/v1/bookcases/01JZ.../rating \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \
  -d '{"value": 5}'
# → returns {userValue, count, average, rounded}. DELETE .../rating removes their rating.

Rate limits & errors

  • Reads: 600 requests / minute, per IP.
  • Writes: 60 requests / 10 minutes, per access token.

Over the limit returns 429 Too Many Requests with a Retry-After header (seconds) and X-RateLimit-Limit. Other errors are JSON with an error message: 400 bad request, 401 missing/invalid token, 403 wrong scope, 404 not found, 422 validation failed (with a violations list).

Data terms & attribution

  • All data on this site is published under the Open Database License (ODbL).
  • If you use this data in another project, that project must also be made available under the ODbL and must include a link to the license text.
  • OpenBookCase contains data imported from OpenStreetMap, so any project using this data must attribute OpenStreetMap and its contributors — a link to the OpenStreetMap copyright page is mandatory.
  • This data must not be imported back into OpenStreetMap. Entries are sometimes added from memory rather than at exact, verified coordinates, which makes them unusable for OpenStreetMap.