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_idand aclient_secretshown 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.
| Scope | Grants |
|---|---|
| bookcases.write | Add & edit entries, move their position |
| bookcases.delete | Delete an entry (a reason is required; a snapshot is archived) |
| images.write | Upload photos to an entry |
| wishlist.write | Add wishlist items to an entry |
| watchlist.write | Watch / unwatch an entry for the user |
| ratings.write | Rate an entry (1–5) for the user, and remove their rating |
| home.write | Set the user's home map location |
Endpoint reference
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/bookcases | open | List by bounding box (latMin,latMax,lonMin,lonMax, limit≤1000, offset) |
| GET | /api/v1/bookcases/{id} | open | Single entry with full detail |
| GET | /api/v1/bookcases/{id}/images | open | List an entry's photos |
| GET | /api/v1/bookcases/{id}/wishlist | open | List an entry's wishlist items |
| GET | /api/v1/bookcases/{id}/caretakers | open | List an entry's caretakers |
| GET | /api/v1/bookcases/{id}/opening-times | open | List an entry's opening times |
| GET | /api/v1/bookcases/{id}/rating | open | Rating summary (count + average) |
| POST | /api/v1/bookcases | bookcases.write | Create an entry |
| PATCH | /api/v1/bookcases/{id} | bookcases.write | Update fields (partial) |
| POST | /api/v1/bookcases/{id}/position | bookcases.write | Move (latitude/longitude) |
| DELETE | /api/v1/bookcases/{id} | bookcases.delete | Delete (JSON reason required) |
| POST | /api/v1/bookcases/{id}/images | images.write | Upload a photo (multipart) |
| POST | /api/v1/bookcases/{id}/wishlist | wishlist.write | Add a wishlist item |
| POST | /api/v1/bookcases/{id}/watch | watchlist.write | Watch an entry |
| DELETE | /api/v1/bookcases/{id}/watch | watchlist.write | Unwatch an entry |
| POST | /api/v1/profile/home | home.write | Set the user's home location |
| POST | /api/v1/bookcases/{id}/caretakers | bookcases.write | Add a caretaker to an entry |
| PATCH | /api/v1/bookcases/{id}/caretakers/{caretakerId} | bookcases.write | Update a caretaker (partial) |
| DELETE | /api/v1/bookcases/{id}/caretakers/{caretakerId} | bookcases.write | Remove a caretaker |
| POST | /api/v1/bookcases/{id}/opening-times | bookcases.write | Add an opening time |
| PATCH | /api/v1/bookcases/{id}/opening-times/{openingTimeId} | bookcases.write | Update an opening time (partial) |
| DELETE | /api/v1/bookcases/{id}/opening-times/{openingTimeId} | bookcases.write | Remove an opening time |
| PUT | /api/v1/bookcases/{id}/rating | ratings.write | Add or update the user's rating (1–5) |
| DELETE | /api/v1/bookcases/{id}/rating | ratings.write | Remove 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.